[{"name":"agent-message-context-usage.md","kind":"file","size":245,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"agent-session-controls.md","kind":"file","size":118,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"background-bash-completion.md","kind":"file","size":82,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"background-signal-completion.md","kind":"file","size":139,"lastCommitHash":"2d59e3a15fb3b68670325e2d9d669993df80019b","lastCommitWhen":"2026-09-17T06:25:27+03:00","lastCommitMessage":"coding-agent/{src/core,test}: report turn sources and signal exits\n"},{"name":"bash-network-perimeter.md","kind":"file","size":435,"lastCommitHash":"e88e3a28a2336ad92bfe06c3725c290fb0e9d54e","lastCommitWhen":"2026-09-17T07:04:26+03:00","lastCommitMessage":"feat(runtime,skills): one calling convention, quiet attenuation, disambiguated edit, network perimeter\n\nR7 S1 (the calling convention) - the cell boundary awaited only coroutines, so a\nskill that returned a plain value made `await skill()` raise \"object X can't be\nused in 'await' expression\" (156 of those in the audit corpus). The evaluator now\nsettles whatever a call returns, `f(x)` and `await f(x)` both work, and every\neager result is settled once; `BashHandle` and a started `asyncio.Task` stay plain\nvalues on purpose, because awaiting them would silently turn `bash(cmd)` into a\nblocking one-shot.\nR7 S4/S5/S6 (runtime half) - one generated surface per skill: a line per published\ncall plus a generated shape line per result type, a byte-capped signature digest,\nand an error boundary that appends the resolved signature, the nearest legal name,\nand the correction for a wrong keyword, a missing positional, awaiting a settled\nvalue, and a wrong attribute. A call that fails the same way twice gets one\nin-band repeat notice (second occurrence, then every fifth) and is never refused.\nThe `signatures` kernel request is additive: an older host never sends it and the\nruntime falls back to indexing the wrapped modules in the cell namespace.\n\nR6 (capability attenuation) - a subagent pushed unreviewed work to origin and ran\ndestructive repository commands (Sev 5, 01a08d13, 01a09632). `git`/`jj` skills now\nrefuse a publish or a force reset/restore from a child unless the parent authorized\nit, and the refusal names the correction (`allow_push=True` at spawn, or\n`RLM_ALLOW_PUSH=1`). Depth-0 sessions are unaffected.\n\nR7 S3/S7 (skills half) - `git`/`jj` take `cwd` for the repository directory and\n`paths=` for target files (`n` -> `limit` on the log/graph calls), the fact/action\nsplit is consistent (collections iterable, shapes generated, no hand-written\nsignature text), and `RemoteHandle.output` is a property.\n\nI1 - a non-unique `old_str` in the `edit` skill now changes nothing and reports the\nbounded line ranges of every occurrence with numbered candidate snippets, and\n`line_hint=<a line inside the wanted range>` resolves it in one retry. Exact-match\nsafety is unchanged.\n\nI6 - a command that would bind a server to a public interface is refused with the\nloopback form to use instead (`RLM_ALLOW_PUBLIC_LISTEN=1` for a deliberate public\nbind), and a hardcoded dev port warns with the ephemeral-port correction. Read-only\npipelines and ssh stay permissive; the R5/R8 guards are untouched.\n\nI5 is closed as superseded by R7 S6.\n\nTests: prime-agent-runtime suite 425; git 15, jj 23, search 11, edit 13 python\ntests; new test_skill_surface.py (20) and test_subagent_workspace.py (16).\n"},{"name":"better-ipython-tool-previews.md","kind":"file","size":191,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"bugbot-inline-separator.md","kind":"file","size":156,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"cell-sleep-guard.md","kind":"file","size":324,"lastCommitHash":"3cfe321e5f4f88df63f58e840fc552cb1f521aeb","lastCommitWhen":"2026-09-17T09:35:27+03:00","lastCommitMessage":"fix(runtime): refuse long cell sleeps and document BashHandle awaitability\n"},{"name":"child-provider-error-is-a-failure.md","kind":"file","size":133,"lastCommitHash":"99e38b6656ab50134976c8833e8cd4bdb7b3b6f5","lastCommitWhen":"2026-09-17T07:06:13+03:00","lastCommitMessage":"fix(coding-agent,runtime): stop three surfaces from quietly disagreeing with reality\n\nThree separate lies, one shape: state that is true somewhere is invisible where the\ndecision is made.\n\n`rlm.models().exhausted` read only the in-memory quota breaker, which is deliberately\nnot persisted, so after any restart it advertised a depleted lane as available — the\nrecorded block in the credential store was the only durable evidence, and `exhausted`\nnever looked at it. A child launched there died at turn 0 with \"You have hit your\nChatGPT usage limit (prolite plan)\" buried in its transcript while the parent saw a\n`completed` row. `exhausted` now unions the breaker with the providers whose stored\ncredentials are all rate-limit blocked (`AuthStorage.blockedProviders`). The spawn gate\nstill refuses only on the breaker: a durable block is a record of one refusal and a\nlane can serve long before its window expires, so refusing on it would brick a working\nprovider — measured live, where opencode-go was recorded blocked for two weeks while\nserving this very session.\n\nA child whose last turn ended in a provider error was reported as `done`, because such\na turn resolves like any other: the parent got a no-reply notice with no cause. It now\ntakes the failure path, carrying the provider's own message.\n\nThe kernel is spawned fresh on resume, so a cell's `os.chdir` was lost while the\nvariables it was used with survived: a later relative path silently resolved somewhere\nelse, and one read-only `git status` in this session ran against the wrong repo because\nof it. The snapshot manifest now records the working directory and the restore\nre-applies it, reporting `cwdMissing` when the directory is gone so the notice says so\ninstead of guessing.\n\nTests: auth-storage block queries, the models/list ambiguity, the child failure notice,\nand the runtime cwd round-trip (including the gone-directory case), each with a\nnegative-control run.\n"},{"name":"commitment-turn-source.md","kind":"file","size":139,"lastCommitHash":"2d59e3a15fb3b68670325e2d9d669993df80019b","lastCommitWhen":"2026-09-17T06:25:27+03:00","lastCommitMessage":"coding-agent/{src/core,test}: report turn sources and signal exits\n"},{"name":"compact-at-threshold.md","kind":"file","size":201,"lastCommitHash":"419643717a968f6cacae6d180d1c60baa070e4ee","lastCommitWhen":"2026-09-17T07:10:10+03:00","lastCommitMessage":"feat(coding-agent): support automatic compaction thresholds via /compact at <tokens>\n"},{"name":"consolidate-async-bash-completions.md","kind":"file","size":238,"lastCommitHash":"a642a0418bf47aeab5a5f3a6743225e86b9511ee","lastCommitWhen":"2026-09-17T06:17:30+03:00","lastCommitMessage":"coding-agent,runtime: consolidate async bash completions and describe remote ssh commands\n"},{"name":"context-pressure-policy.md","kind":"file","size":688,"lastCommitHash":"f781e33779d4d57203f21db2d19746fa5b23a3c6","lastCommitWhen":"2026-09-17T07:04:08+03:00","lastCommitMessage":"feat(coding-agent): context is the harness's job, not a budget the model watches (P1, I3, P2)\n\nP1 - a context-budget panic stopped work at 11% usage (01a0920d) because the\nmodel read a compaction counter as an external budget; the corpus also shows\nmodels shortening answers, refusing to continue, and treating `percent` as a\nthreshold. Nine false-budget signals reached a model or a parent. They are gone:\n\n- the compact skill's description no longer says \"context is filling up\" and no\n  longer tells the model to compact while work remains; its rules now say\n  context size is not a budget the model manages and is never a reason to stop,\n  shorten, drop a step, or return early, and `status()` is named as a\n  measurement rather than a limit\n- the system prompt's REPL paragraph drops \"context management\" and the\n  variable-size compaction sentence, and the long-running-work block gains the\n  rule outright: context is never a blocker\n- the context-overflow and compaction-failure messages ask for the last real\n  blocker instead of suggesting a smaller context or a larger-context model\n- goal `remaining_tokens` is labelled a lifetime goal budget on both goal skill\n  surfaces and in the goal context messages, not context headroom\n- the autonomous continuation prompt points at the host that stops the run\n  instead of an unquantified budget\n- the compaction-summary suffix states once that a mid-task compaction is\n  routine: continue without an apology, without restating the plan, and without\n  trimming the work\n- session-local continual harness entries now win the digest's detail budget, so\n  a large global harness can no longer push this session's own memories into the\n  listing-only tail at compaction (this also fixes a failing suite test)\n\nThe harness still compacts automatically at a turn boundary and resumes the\nmodel with the summary; a checkpoint is for summary quality only (the user\nasked, or a large milestone closed). No field, threshold, or reserve is\npublished to the model: the user and the UI keep the numbers.\n\nI3 - a reply that drifts out of the conversation's script (01a0924e, 01a09b11)\nnow gets one advisory `[language-drift]` notice, at most three per session and\nnever twice within twenty assistant turns. It is advisory only: short or\ncode-heavy replies, and a user who writes that script, are ignored.\n\nP2 - the child-lifetime policy, as policy only: the system prompt states one\nchild per coherent deliverable, reuse only for live state, never tell a child to\ncompact or to stay short, and no turn/token/lifetime caps. The parent-side\nfooter now reports `compactionCount` from `ContextUsage` -- the lifetime signal\na parent should read -- and labels the token numbers as measured, not a limit.\n"},{"name":"daemon-reattach-fresh-session.md","kind":"file","size":239,"lastCommitHash":"bebad881b47634cff2ea4322af7c151b19fd8536","lastCommitWhen":"2026-09-17T14:54:49+03:00","lastCommitMessage":"fix(coding-agent): recover a window whose daemon session is unrestorable instead of exiting\n"},{"name":"daemon-reattach-resilience.md","kind":"file","size":146,"lastCommitHash":"02aa818cde550ab1d90df02c6a0547cd93287160","lastCommitWhen":"2026-09-17T12:50:51+03:00","lastCommitMessage":"fix(coding-agent): survive daemon restarts and retry re-attachment with backoff\n"},{"name":"daemon-reattach-v6-hardening.md","kind":"file","size":217,"lastCommitHash":"8cc9f73a04190075799e13e31009a837444bdcf7","lastCommitWhen":"2026-09-17T20:43:05+03:00","lastCommitMessage":"fix(coding-agent): harden daemon reattach remap verdicts, replacement concurrency, and disposal\n"},{"name":"daemon-restart-path.md","kind":"file","size":119,"lastCommitHash":"8f13aae5e8c769556c6c2a36469b825766c9aa7a","lastCommitWhen":"2026-09-17T21:59:08+03:00","lastCommitMessage":"feat(coding-agent): add supported daemon restart path restoring active sessions\n"},{"name":"daemon-stale-state.md","kind":"file","size":138,"lastCommitHash":"4ebf5f8aa1aa527a5c2a3dae40ceefa4a34afe32","lastCommitWhen":"2026-09-17T20:51:17+03:00","lastCommitMessage":"fix(coding-agent): reclaim stale session state, bound worker recovery, and heal via doctor\n"},{"name":"daemon-stop-detached-client.md","kind":"file","size":88,"lastCommitHash":"2dc73bb8cf2d09d1f3c8b44f4b09eb01d98f7afd","lastCommitWhen":"2026-09-17T06:41:46+03:00","lastCommitMessage":"coding-agent/modes/daemon: fix stop when the client already disconnected\n"},{"name":"daemon-worker-crash-policy.md","kind":"file","size":116,"lastCommitHash":"9195f9e3e91e6df686af6c9baf01f31fe81f2917","lastCommitWhen":"2026-09-17T06:41:46+03:00","lastCommitMessage":"coding-agent/modes/daemon: keep workers alive on unhandled rejections\n"},{"name":"deepseek-flash.md","kind":"file","size":75,"lastCommitHash":"db7ff1c1e8f8f3f3db17285b028f8e4c872e3638","lastCommitWhen":"2026-09-17T07:01:44+03:00","lastCommitMessage":"fix(ai): update deepseek flash model to deepseek/deepseek-flash\n"},{"name":"dot-continuation.md","kind":"file","size":68,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"edit-line-hint-disambiguation.md","kind":"file","size":279,"lastCommitHash":"e88e3a28a2336ad92bfe06c3725c290fb0e9d54e","lastCommitWhen":"2026-09-17T07:04:26+03:00","lastCommitMessage":"feat(runtime,skills): one calling convention, quiet attenuation, disambiguated edit, network perimeter\n\nR7 S1 (the calling convention) - the cell boundary awaited only coroutines, so a\nskill that returned a plain value made `await skill()` raise \"object X can't be\nused in 'await' expression\" (156 of those in the audit corpus). The evaluator now\nsettles whatever a call returns, `f(x)` and `await f(x)` both work, and every\neager result is settled once; `BashHandle` and a started `asyncio.Task` stay plain\nvalues on purpose, because awaiting them would silently turn `bash(cmd)` into a\nblocking one-shot.\nR7 S4/S5/S6 (runtime half) - one generated surface per skill: a line per published\ncall plus a generated shape line per result type, a byte-capped signature digest,\nand an error boundary that appends the resolved signature, the nearest legal name,\nand the correction for a wrong keyword, a missing positional, awaiting a settled\nvalue, and a wrong attribute. A call that fails the same way twice gets one\nin-band repeat notice (second occurrence, then every fifth) and is never refused.\nThe `signatures` kernel request is additive: an older host never sends it and the\nruntime falls back to indexing the wrapped modules in the cell namespace.\n\nR6 (capability attenuation) - a subagent pushed unreviewed work to origin and ran\ndestructive repository commands (Sev 5, 01a08d13, 01a09632). `git`/`jj` skills now\nrefuse a publish or a force reset/restore from a child unless the parent authorized\nit, and the refusal names the correction (`allow_push=True` at spawn, or\n`RLM_ALLOW_PUSH=1`). Depth-0 sessions are unaffected.\n\nR7 S3/S7 (skills half) - `git`/`jj` take `cwd` for the repository directory and\n`paths=` for target files (`n` -> `limit` on the log/graph calls), the fact/action\nsplit is consistent (collections iterable, shapes generated, no hand-written\nsignature text), and `RemoteHandle.output` is a property.\n\nI1 - a non-unique `old_str` in the `edit` skill now changes nothing and reports the\nbounded line ranges of every occurrence with numbered candidate snippets, and\n`line_hint=<a line inside the wanted range>` resolves it in one retry. Exact-match\nsafety is unchanged.\n\nI6 - a command that would bind a server to a public interface is refused with the\nloopback form to use instead (`RLM_ALLOW_PUBLIC_LISTEN=1` for a deliberate public\nbind), and a hardcoded dev port warns with the ephemeral-port correction. Read-only\npipelines and ssh stay permissive; the R5/R8 guards are untouched.\n\nI5 is closed as superseded by R7 S6.\n\nTests: prime-agent-runtime suite 425; git 15, jj 23, search 11, edit 13 python\ntests; new test_skill_surface.py (20) and test_subagent_workspace.py (16).\n"},{"name":"eng-6108-accounts-menu-redesign.md","kind":"file","size":962,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-alias-provider-classification.md","kind":"file","size":704,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-catalog-final-cut.md","kind":"file","size":493,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-connect-message-nits.md","kind":"file","size":587,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-generic-mcp-discovery.md","kind":"file","size":612,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-lifecycle-hardening.md","kind":"file","size":381,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-mcp-account-ownership.md","kind":"file","size":1661,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-mcp-bootstrap-ready-check.md","kind":"file","size":243,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-mcp-login-inline.md","kind":"file","size":389,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-paste-token-flow.md","kind":"file","size":2362,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-pr2256-oauth-engine.md","kind":"file","size":638,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-recovery-edges.md","kind":"file","size":3929,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-service-catalog.md","kind":"file","size":5819,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"eng-6108-zero-app-catalog-cut.md","kind":"file","size":526,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"exhausted-names-recorded-blocks.md","kind":"file","size":154,"lastCommitHash":"99e38b6656ab50134976c8833e8cd4bdb7b3b6f5","lastCommitWhen":"2026-09-17T07:06:13+03:00","lastCommitMessage":"fix(coding-agent,runtime): stop three surfaces from quietly disagreeing with reality\n\nThree separate lies, one shape: state that is true somewhere is invisible where the\ndecision is made.\n\n`rlm.models().exhausted` read only the in-memory quota breaker, which is deliberately\nnot persisted, so after any restart it advertised a depleted lane as available — the\nrecorded block in the credential store was the only durable evidence, and `exhausted`\nnever looked at it. A child launched there died at turn 0 with \"You have hit your\nChatGPT usage limit (prolite plan)\" buried in its transcript while the parent saw a\n`completed` row. `exhausted` now unions the breaker with the providers whose stored\ncredentials are all rate-limit blocked (`AuthStorage.blockedProviders`). The spawn gate\nstill refuses only on the breaker: a durable block is a record of one refusal and a\nlane can serve long before its window expires, so refusing on it would brick a working\nprovider — measured live, where opencode-go was recorded blocked for two weeks while\nserving this very session.\n\nA child whose last turn ended in a provider error was reported as `done`, because such\na turn resolves like any other: the parent got a no-reply notice with no cause. It now\ntakes the failure path, carrying the provider's own message.\n\nThe kernel is spawned fresh on resume, so a cell's `os.chdir` was lost while the\nvariables it was used with survived: a later relative path silently resolved somewhere\nelse, and one read-only `git status` in this session ran against the wrong repo because\nof it. The snapshot manifest now records the working directory and the restore\nre-applies it, reporting `cwdMissing` when the directory is gone so the notice says so\ninstead of guessing.\n\nTests: auth-storage block queries, the models/list ambiguity, the child failure notice,\nand the runtime cwd round-trip (including the gone-directory case), each with a\nnegative-control run.\n"},{"name":"extension-command-argument-completions.md","kind":"file","size":249,"lastCommitHash":"c8b6cbb670a8e1f1c1384620c1144d52cdf967fb","lastCommitWhen":"2026-09-17T07:09:58+03:00","lastCommitMessage":"feat(coding-agent): complete extension command arguments in daemon-attached clients\n"},{"name":"fix-daemon-shutdown-admission-race.md","kind":"file","size":137,"lastCommitHash":"cf07c5a3f5eca98e7744f2df83050044c920252a","lastCommitWhen":"2026-09-16T16:28:13-04:00","lastCommitMessage":"delete dead tests and fix the races the flaky ones were hiding (#2336)\n\n* add an objective testing policy to AGENTS.md\n\n* fix flaky daemon supervisor monitor test\n\nThe supervisor availability check is a real async chain (registry stat,\nsocket probe, replacement launch) started from a timer callback, so the\ntest drove a bounded fake-timer pump and asserted on a probe counter.\nUnder a loaded CI shard the chain did not finish inside the iteration\nbudget, and the variant mixing fake timers with a real probe promise\ncould deadlock for the full 30s test timeout.\n\nExpose a settled promise per armed availability check and await it in\nthe tests, so each assertion runs after the check finished instead of\nafter an arbitrary number of clock advances.\n\n* remove vitest retry masking from ai and attach-image tests\n\nDeleted all 548 `retry:` options in packages/ai/test and the 2 in\nkernel-attach-image-skill.test.ts. Retries hid non-determinism instead of\nfixing it, so a broken code path could still go green.\n\n- Deleted 17 live-only cross-provider E2E matrices whose every case sits behind\n  skipIf(!API_KEY) and never runs in CI; stream.test.ts stays as the survivor\n  that owns the provider table.\n- Stripped the retry option from the 198 stream.test.ts cases and dropped the\n  live E2E describe (plus its now-unused helpers) from\n  anthropic-thinking-disable.test.ts, keeping its mocked payload contract.\n- Removed retry: 1 from the two attach-image kernel cases; they pass without it.\n\nMocked wire/SSE contract tests are untouched. Tests only, no product code.\n\n* make agent trace upload tests deterministic\n\nThe suite pumped a synthetic clock with advanceTimersUntil() and hoped real\nfs and promise work settled inside a 200-step budget, asserted schedule\ndelays through a global setTimeout spy whose fake clock had drifted, and\nremoved its temp dir while catch-up and cursor writers were still running.\nUnder a loaded CI shard that produced \"Timed out advancing fake timers\",\n\"expected 1000 to be 60000\" and \"ENOTEMPTY rmdir /tmp/agent-traces-test-*\".\n\nThe upload scheduler now reports the delay it arms and the outcome of each\ncycle, the upload reports every wait it arms before the next request, and\ninstallAgentTraceUpload returns a handle that resolves when the startup\ncatch-up and any in-flight upload have finished writing. Tests await those\nsignals and advance the fake clock by the exact reported delay, and teardown\nwaits for in-flight writers before removing the temp dir.\n\nThe ~15 retry, backoff and Retry-After variants collapse into one\ntable-driven it.each, per the MERGE verdict: 1569 -> 1141 lines, 44 -> 27\ntest blocks, with no lost coverage.\n\n* make the compiled artifact and installer probe tests deterministic\n\nThe compiled-artifact afterEach polled for supervisor exit with the same\n10s budget as vitest's hook timeout, so the poll could never report on the\nslower standalone runners. The teardown now escalates to SIGKILL and every\ninner deadline stays below the surrounding hook and test budgets.\n\nnative-probe-timeout decided pass/fail from wall-clock deadlines around a\nreal 10s installer probe. The installer probe deadline is now injectable\nthrough PRIME_AGENT_PROBE_TIMEOUT_SECONDS, the blocked probe records the\ninstall lock itself, and the spawn deadlines are gone.\n\n* fix daemon shutdown admission loss during slow scans\n\nshutdown --force holds a 5s admission lease refreshed by a 1s timer, but it\nblocks its own event loop in synchronous ps/lsof/ss scans. On a loaded runner a\nscan outlasts the lease, the elapsed record is treated as abandoned, and the\nholder's next assertOrRenew throws \"Daemon shutdown admission was lost\".\n\n- A late renew re-arms a record that is still ours; only a missing or foreign\n  record ends the admission.\n- A renew that could not read the registry no longer retires the lease.\n- isDaemonShutdownAdmissionActive is read-only, so a bystander probe cannot\n  delete a live holder's record.\n- stopHiddenSupervisors renews before the blocking scan.\n\nThe 4603 regression drops its 11s sleep for a post-shutdown listener assertion\nand uses mkdtemp for the child TMPDIR.\n\n* delete 148 dead test files\n\nRemoves test files judged CUT by a full 480-file audit: duplicates of a named survivor, mock-asserting tests, rendered-copy assertions, private-internals casts, and live-API matrices behind skipIf(!API_KEY) that never ran in CI.\n\n* merge interactive-mode and agents-view test suites\n\n* merge tui render, overlay and helper test suites\n\n* test: slim daemon-mode and agent-connection-daemon suites\n\ndaemon-mode: keep passive hydration/passivation, delete_subagent durability, agent-message concurrency and rate limits, attach-snapshot backpressure; drop per-race internal regressions already covered process-level by daemon-supervisor-process.\nagent-connection-daemon: keep reconnect, update-restart, chunked-snapshot assembly and disposal races; drop tautological daemon-protocol pass-through cases.\n\n* merge tui editor, markdown and autocomplete test suites\n\n* test: merge audited session-core test bloat\n\nExecute MERGE verdicts from the 480-file test audit for the agent-session\ncore suites: collapse near-identical blocks into it.each tables, drop\nmock-return/rendered-copy assertions and private-cast white-box cases,\nand fold one-off files into the owning suite.\n\nDeleted (folded into their owning suite):\n- agent-session-concurrent.test.ts, suite/agent-session-action-races.test.ts,\n  suite/agent-session-action-contracts.test.ts -> suite/agent-session-queue.test.ts\n- suite/agent-session-compaction-continuation.test.ts,\n  suite/agent-session-compact-skill.test.ts -> suite/agent-session-compaction.test.ts\n- agent-session-runtime-events.test.ts -> suite/agent-session-runtime.test.ts\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency/ordering, crash/restart/resume and load coverage.\n\n* address review: octal probe timeout, truncated deadline, dead schedule field\n\n* test: merge redundant coding-agent test cases into case tables\n\nExecutes the MERGE verdicts from the 480-file test-bloat audit for the\nmisc lanes. Repeated near-identical blocks collapse into single it.each\ncase tables, and ticket-numbered one-off files fold into the owning\nmodule suite.\n\nRemoved throughout the touched files: assertions on mock return values,\nassertions on rendered text/copy, `as unknown as` casts into privates,\nand suites gated behind skipIf(!API_KEY) (which never ran in CI).\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency and ordering, crash/restart/resume, and load.\n\nDeleted files, folded into their module suite:\n- compaction-extensions -> deleted outright (whole file behind skipIf(!API_KEY))\n- compaction-serialization -> compaction\n- heartbeat-catalog -> daemon-client\n- child-process -> exec\n- edit-tool-legacy-input -> tools\n- kernel-goal-skill, kernel-rlm-heartbeat-skill -> kernel-agent-message-skill\n- kernel-agent-observe-skill -> kernel-attach-image-skill\n- kernel-windows-process -> kernel-bootstrap-windows\n- extensions-input-event -> extensions-runner\n- sdk-skills -> sdk-session-manager\n- session-manager/{agent-status,has-user-content} -> session-manager/session-state\n- session-manager/migration -> session-manager/file-operations\n- session-manager/custom-session-id -> session-manager/tree-traversal\n\n* slim ai + regression tests (lanes A/B/D/E)\n\nExecute MERGE verdicts from the test audit for packages/ai and the\nupdate-restart / snapshot / session-view regression files.\n\n- fold ticket-numbered regression cases into the owning suites\n- collapse near-identical blocks into it.each tables\n- drop mock-return, rendered-copy and live-API-gated assertions\n\n* test: collapse package-manager tables and fold small daemon selector suites\n\npackage-manager: git-URL parsing, top-level/manifest/package glob filters and npm install-argv cases are now table-driven.\ndaemon-session-id: absorbs daemon-catalog-process selector case, drops display-id formatting trivia.\ndaemon-worker-windows-timeouts: absorbs the connection-deadline budget cases from daemon-worker-connect.\ndaemon-catalog-startup: drops the src-directory one-off.\n\n* test: collapse refinement, settings and config suites into case tables\n\nFinal misc lane of the test-bloat audit.\n\n- refinement: per-kind create/update/delete becomes one it.each lifecycle;\n  8 rejection suites become one 18-row invalid-edit table that also asserts\n  state and the refinement log stay untouched; truncation/parse errors,\n  scope permutations, ranking and the tokenizer each become one table.\n  System-prompt and user-prompt copy assertions removed.\n- settings-manager: per-setting default getters removed; unknown-key\n  preservation, invalid-file guards and project-vs-global precedence kept.\n- config: install-method detection becomes one table; session-dir env\n  permutations collapse into one precedence test.\n- builtin-skills, repl-kernel-abort, session-cwd, truncate: repeated blocks\n  collapsed; one ReplInternals reach-in dropped.\n- child-process folds into exec as one process lifecycle suite.\n\n* slim regression tests (lane C)\n\nFold the remaining ticket-numbered regression files into the owning\nmodule suites and collapse duplicate cases into it.each tables.\n\n- 19 regression files deleted after their kept cases moved into\n  agent-session-queue, auth-storage, acp-features, resource-loader,\n  agent-session-runtime/compaction/retry-events/recursion,\n  model-registry, slash-commands, daemon-supervisor-process\n- 3 files slimmed in place where the named target no longer exists\n- drop rendered-copy, mock-return and private-cast assertions\n\n* test: collapse daemon protocol, command, list, ps and package-command suites\n\ndaemon-protocol: two gating tables replace 16 near-identical capability/schema cases.\ndaemon-command: table-driven argv parsing plus the --goal leak guards; drops help/usage copy assertions.\ndaemon-session-list: activity, recap and model-fallback projections are table-driven.\ndaemon-ps: keeps planReap/planShutdownAll safety rules, drops ss/lsof parsers, sorting and uptime trivia.\ndaemon-stop-confirm: one busy-guard table, no prompt copy.\npackage-command-paths: one shared self-update fixture for the install-spec cases; drops help and removed-flag copy.\nacp-events: keeps only the cases the ACP suite does not cover end to end.\ndaemon-session-summarizer: one recap/status parse table.\ndaemon-extension-binding: keeps the duplicated-partial-message strip.\n\n* test: tighten the daemon stop-confirm busy-guard table\n\n* fix ci: biome forEach body, vacuous autocomplete case, missing prompt before second compaction\n\n* fix ci: restore escaped template literals in prompt-template cases\n\n* fix codeql: single-pass path escaping in test helpers\n\n* address review: flush debounced trace uploads, restore probe timeout guards, drop dead test scaffolding\n\n* respect retry-after and throttle window when flushing trace uploads\n\n* await in-flight trace uploads before a drain returns\n\n* extend deterministic test policy to python\n\n* harden deterministic test policy checks\n\n* close test policy edge cases\n\n* finish deterministic policy edge coverage\n\n* fix policy lexer and macos signature setup\n\n* mask regex literals in test policy checks\n\n* distinguish postfix division in policy lexer\n\n* close native migration signal race\n\n* keep native migration force timer owned\n\n---------\n\nCo-authored-by: prime-agent <agent@local>"},{"name":"fix-models-dev-logging.md","kind":"file","size":88,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"fix-spawn-call-contract-docs.md","kind":"file","size":146,"lastCommitHash":"c71c7c55651f72e5257a6ae776116d356addc2dd","lastCommitWhen":"2026-09-17T08:57:27+03:00","lastCommitMessage":"fix(coding-agent,runtime): align documented rlm.spawn call contract with implementation\n"},{"name":"flaky-agent-traces.md","kind":"file","size":156,"lastCommitHash":"cf07c5a3f5eca98e7744f2df83050044c920252a","lastCommitWhen":"2026-09-16T16:28:13-04:00","lastCommitMessage":"delete dead tests and fix the races the flaky ones were hiding (#2336)\n\n* add an objective testing policy to AGENTS.md\n\n* fix flaky daemon supervisor monitor test\n\nThe supervisor availability check is a real async chain (registry stat,\nsocket probe, replacement launch) started from a timer callback, so the\ntest drove a bounded fake-timer pump and asserted on a probe counter.\nUnder a loaded CI shard the chain did not finish inside the iteration\nbudget, and the variant mixing fake timers with a real probe promise\ncould deadlock for the full 30s test timeout.\n\nExpose a settled promise per armed availability check and await it in\nthe tests, so each assertion runs after the check finished instead of\nafter an arbitrary number of clock advances.\n\n* remove vitest retry masking from ai and attach-image tests\n\nDeleted all 548 `retry:` options in packages/ai/test and the 2 in\nkernel-attach-image-skill.test.ts. Retries hid non-determinism instead of\nfixing it, so a broken code path could still go green.\n\n- Deleted 17 live-only cross-provider E2E matrices whose every case sits behind\n  skipIf(!API_KEY) and never runs in CI; stream.test.ts stays as the survivor\n  that owns the provider table.\n- Stripped the retry option from the 198 stream.test.ts cases and dropped the\n  live E2E describe (plus its now-unused helpers) from\n  anthropic-thinking-disable.test.ts, keeping its mocked payload contract.\n- Removed retry: 1 from the two attach-image kernel cases; they pass without it.\n\nMocked wire/SSE contract tests are untouched. Tests only, no product code.\n\n* make agent trace upload tests deterministic\n\nThe suite pumped a synthetic clock with advanceTimersUntil() and hoped real\nfs and promise work settled inside a 200-step budget, asserted schedule\ndelays through a global setTimeout spy whose fake clock had drifted, and\nremoved its temp dir while catch-up and cursor writers were still running.\nUnder a loaded CI shard that produced \"Timed out advancing fake timers\",\n\"expected 1000 to be 60000\" and \"ENOTEMPTY rmdir /tmp/agent-traces-test-*\".\n\nThe upload scheduler now reports the delay it arms and the outcome of each\ncycle, the upload reports every wait it arms before the next request, and\ninstallAgentTraceUpload returns a handle that resolves when the startup\ncatch-up and any in-flight upload have finished writing. Tests await those\nsignals and advance the fake clock by the exact reported delay, and teardown\nwaits for in-flight writers before removing the temp dir.\n\nThe ~15 retry, backoff and Retry-After variants collapse into one\ntable-driven it.each, per the MERGE verdict: 1569 -> 1141 lines, 44 -> 27\ntest blocks, with no lost coverage.\n\n* make the compiled artifact and installer probe tests deterministic\n\nThe compiled-artifact afterEach polled for supervisor exit with the same\n10s budget as vitest's hook timeout, so the poll could never report on the\nslower standalone runners. The teardown now escalates to SIGKILL and every\ninner deadline stays below the surrounding hook and test budgets.\n\nnative-probe-timeout decided pass/fail from wall-clock deadlines around a\nreal 10s installer probe. The installer probe deadline is now injectable\nthrough PRIME_AGENT_PROBE_TIMEOUT_SECONDS, the blocked probe records the\ninstall lock itself, and the spawn deadlines are gone.\n\n* fix daemon shutdown admission loss during slow scans\n\nshutdown --force holds a 5s admission lease refreshed by a 1s timer, but it\nblocks its own event loop in synchronous ps/lsof/ss scans. On a loaded runner a\nscan outlasts the lease, the elapsed record is treated as abandoned, and the\nholder's next assertOrRenew throws \"Daemon shutdown admission was lost\".\n\n- A late renew re-arms a record that is still ours; only a missing or foreign\n  record ends the admission.\n- A renew that could not read the registry no longer retires the lease.\n- isDaemonShutdownAdmissionActive is read-only, so a bystander probe cannot\n  delete a live holder's record.\n- stopHiddenSupervisors renews before the blocking scan.\n\nThe 4603 regression drops its 11s sleep for a post-shutdown listener assertion\nand uses mkdtemp for the child TMPDIR.\n\n* delete 148 dead test files\n\nRemoves test files judged CUT by a full 480-file audit: duplicates of a named survivor, mock-asserting tests, rendered-copy assertions, private-internals casts, and live-API matrices behind skipIf(!API_KEY) that never ran in CI.\n\n* merge interactive-mode and agents-view test suites\n\n* merge tui render, overlay and helper test suites\n\n* test: slim daemon-mode and agent-connection-daemon suites\n\ndaemon-mode: keep passive hydration/passivation, delete_subagent durability, agent-message concurrency and rate limits, attach-snapshot backpressure; drop per-race internal regressions already covered process-level by daemon-supervisor-process.\nagent-connection-daemon: keep reconnect, update-restart, chunked-snapshot assembly and disposal races; drop tautological daemon-protocol pass-through cases.\n\n* merge tui editor, markdown and autocomplete test suites\n\n* test: merge audited session-core test bloat\n\nExecute MERGE verdicts from the 480-file test audit for the agent-session\ncore suites: collapse near-identical blocks into it.each tables, drop\nmock-return/rendered-copy assertions and private-cast white-box cases,\nand fold one-off files into the owning suite.\n\nDeleted (folded into their owning suite):\n- agent-session-concurrent.test.ts, suite/agent-session-action-races.test.ts,\n  suite/agent-session-action-contracts.test.ts -> suite/agent-session-queue.test.ts\n- suite/agent-session-compaction-continuation.test.ts,\n  suite/agent-session-compact-skill.test.ts -> suite/agent-session-compaction.test.ts\n- agent-session-runtime-events.test.ts -> suite/agent-session-runtime.test.ts\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency/ordering, crash/restart/resume and load coverage.\n\n* address review: octal probe timeout, truncated deadline, dead schedule field\n\n* test: merge redundant coding-agent test cases into case tables\n\nExecutes the MERGE verdicts from the 480-file test-bloat audit for the\nmisc lanes. Repeated near-identical blocks collapse into single it.each\ncase tables, and ticket-numbered one-off files fold into the owning\nmodule suite.\n\nRemoved throughout the touched files: assertions on mock return values,\nassertions on rendered text/copy, `as unknown as` casts into privates,\nand suites gated behind skipIf(!API_KEY) (which never ran in CI).\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency and ordering, crash/restart/resume, and load.\n\nDeleted files, folded into their module suite:\n- compaction-extensions -> deleted outright (whole file behind skipIf(!API_KEY))\n- compaction-serialization -> compaction\n- heartbeat-catalog -> daemon-client\n- child-process -> exec\n- edit-tool-legacy-input -> tools\n- kernel-goal-skill, kernel-rlm-heartbeat-skill -> kernel-agent-message-skill\n- kernel-agent-observe-skill -> kernel-attach-image-skill\n- kernel-windows-process -> kernel-bootstrap-windows\n- extensions-input-event -> extensions-runner\n- sdk-skills -> sdk-session-manager\n- session-manager/{agent-status,has-user-content} -> session-manager/session-state\n- session-manager/migration -> session-manager/file-operations\n- session-manager/custom-session-id -> session-manager/tree-traversal\n\n* slim ai + regression tests (lanes A/B/D/E)\n\nExecute MERGE verdicts from the test audit for packages/ai and the\nupdate-restart / snapshot / session-view regression files.\n\n- fold ticket-numbered regression cases into the owning suites\n- collapse near-identical blocks into it.each tables\n- drop mock-return, rendered-copy and live-API-gated assertions\n\n* test: collapse package-manager tables and fold small daemon selector suites\n\npackage-manager: git-URL parsing, top-level/manifest/package glob filters and npm install-argv cases are now table-driven.\ndaemon-session-id: absorbs daemon-catalog-process selector case, drops display-id formatting trivia.\ndaemon-worker-windows-timeouts: absorbs the connection-deadline budget cases from daemon-worker-connect.\ndaemon-catalog-startup: drops the src-directory one-off.\n\n* test: collapse refinement, settings and config suites into case tables\n\nFinal misc lane of the test-bloat audit.\n\n- refinement: per-kind create/update/delete becomes one it.each lifecycle;\n  8 rejection suites become one 18-row invalid-edit table that also asserts\n  state and the refinement log stay untouched; truncation/parse errors,\n  scope permutations, ranking and the tokenizer each become one table.\n  System-prompt and user-prompt copy assertions removed.\n- settings-manager: per-setting default getters removed; unknown-key\n  preservation, invalid-file guards and project-vs-global precedence kept.\n- config: install-method detection becomes one table; session-dir env\n  permutations collapse into one precedence test.\n- builtin-skills, repl-kernel-abort, session-cwd, truncate: repeated blocks\n  collapsed; one ReplInternals reach-in dropped.\n- child-process folds into exec as one process lifecycle suite.\n\n* slim regression tests (lane C)\n\nFold the remaining ticket-numbered regression files into the owning\nmodule suites and collapse duplicate cases into it.each tables.\n\n- 19 regression files deleted after their kept cases moved into\n  agent-session-queue, auth-storage, acp-features, resource-loader,\n  agent-session-runtime/compaction/retry-events/recursion,\n  model-registry, slash-commands, daemon-supervisor-process\n- 3 files slimmed in place where the named target no longer exists\n- drop rendered-copy, mock-return and private-cast assertions\n\n* test: collapse daemon protocol, command, list, ps and package-command suites\n\ndaemon-protocol: two gating tables replace 16 near-identical capability/schema cases.\ndaemon-command: table-driven argv parsing plus the --goal leak guards; drops help/usage copy assertions.\ndaemon-session-list: activity, recap and model-fallback projections are table-driven.\ndaemon-ps: keeps planReap/planShutdownAll safety rules, drops ss/lsof parsers, sorting and uptime trivia.\ndaemon-stop-confirm: one busy-guard table, no prompt copy.\npackage-command-paths: one shared self-update fixture for the install-spec cases; drops help and removed-flag copy.\nacp-events: keeps only the cases the ACP suite does not cover end to end.\ndaemon-session-summarizer: one recap/status parse table.\ndaemon-extension-binding: keeps the duplicated-partial-message strip.\n\n* test: tighten the daemon stop-confirm busy-guard table\n\n* fix ci: biome forEach body, vacuous autocomplete case, missing prompt before second compaction\n\n* fix ci: restore escaped template literals in prompt-template cases\n\n* fix codeql: single-pass path escaping in test helpers\n\n* address review: flush debounced trace uploads, restore probe timeout guards, drop dead test scaffolding\n\n* respect retry-after and throttle window when flushing trace uploads\n\n* await in-flight trace uploads before a drain returns\n\n* extend deterministic test policy to python\n\n* harden deterministic test policy checks\n\n* close test policy edge cases\n\n* finish deterministic policy edge coverage\n\n* fix policy lexer and macos signature setup\n\n* mask regex literals in test policy checks\n\n* distinguish postfix division in policy lexer\n\n* close native migration signal race\n\n* keep native migration force timer owned\n\n---------\n\nCo-authored-by: prime-agent <agent@local>"},{"name":"flaky-artifact-and-probe.md","kind":"file","size":140,"lastCommitHash":"cf07c5a3f5eca98e7744f2df83050044c920252a","lastCommitWhen":"2026-09-16T16:28:13-04:00","lastCommitMessage":"delete dead tests and fix the races the flaky ones were hiding (#2336)\n\n* add an objective testing policy to AGENTS.md\n\n* fix flaky daemon supervisor monitor test\n\nThe supervisor availability check is a real async chain (registry stat,\nsocket probe, replacement launch) started from a timer callback, so the\ntest drove a bounded fake-timer pump and asserted on a probe counter.\nUnder a loaded CI shard the chain did not finish inside the iteration\nbudget, and the variant mixing fake timers with a real probe promise\ncould deadlock for the full 30s test timeout.\n\nExpose a settled promise per armed availability check and await it in\nthe tests, so each assertion runs after the check finished instead of\nafter an arbitrary number of clock advances.\n\n* remove vitest retry masking from ai and attach-image tests\n\nDeleted all 548 `retry:` options in packages/ai/test and the 2 in\nkernel-attach-image-skill.test.ts. Retries hid non-determinism instead of\nfixing it, so a broken code path could still go green.\n\n- Deleted 17 live-only cross-provider E2E matrices whose every case sits behind\n  skipIf(!API_KEY) and never runs in CI; stream.test.ts stays as the survivor\n  that owns the provider table.\n- Stripped the retry option from the 198 stream.test.ts cases and dropped the\n  live E2E describe (plus its now-unused helpers) from\n  anthropic-thinking-disable.test.ts, keeping its mocked payload contract.\n- Removed retry: 1 from the two attach-image kernel cases; they pass without it.\n\nMocked wire/SSE contract tests are untouched. Tests only, no product code.\n\n* make agent trace upload tests deterministic\n\nThe suite pumped a synthetic clock with advanceTimersUntil() and hoped real\nfs and promise work settled inside a 200-step budget, asserted schedule\ndelays through a global setTimeout spy whose fake clock had drifted, and\nremoved its temp dir while catch-up and cursor writers were still running.\nUnder a loaded CI shard that produced \"Timed out advancing fake timers\",\n\"expected 1000 to be 60000\" and \"ENOTEMPTY rmdir /tmp/agent-traces-test-*\".\n\nThe upload scheduler now reports the delay it arms and the outcome of each\ncycle, the upload reports every wait it arms before the next request, and\ninstallAgentTraceUpload returns a handle that resolves when the startup\ncatch-up and any in-flight upload have finished writing. Tests await those\nsignals and advance the fake clock by the exact reported delay, and teardown\nwaits for in-flight writers before removing the temp dir.\n\nThe ~15 retry, backoff and Retry-After variants collapse into one\ntable-driven it.each, per the MERGE verdict: 1569 -> 1141 lines, 44 -> 27\ntest blocks, with no lost coverage.\n\n* make the compiled artifact and installer probe tests deterministic\n\nThe compiled-artifact afterEach polled for supervisor exit with the same\n10s budget as vitest's hook timeout, so the poll could never report on the\nslower standalone runners. The teardown now escalates to SIGKILL and every\ninner deadline stays below the surrounding hook and test budgets.\n\nnative-probe-timeout decided pass/fail from wall-clock deadlines around a\nreal 10s installer probe. The installer probe deadline is now injectable\nthrough PRIME_AGENT_PROBE_TIMEOUT_SECONDS, the blocked probe records the\ninstall lock itself, and the spawn deadlines are gone.\n\n* fix daemon shutdown admission loss during slow scans\n\nshutdown --force holds a 5s admission lease refreshed by a 1s timer, but it\nblocks its own event loop in synchronous ps/lsof/ss scans. On a loaded runner a\nscan outlasts the lease, the elapsed record is treated as abandoned, and the\nholder's next assertOrRenew throws \"Daemon shutdown admission was lost\".\n\n- A late renew re-arms a record that is still ours; only a missing or foreign\n  record ends the admission.\n- A renew that could not read the registry no longer retires the lease.\n- isDaemonShutdownAdmissionActive is read-only, so a bystander probe cannot\n  delete a live holder's record.\n- stopHiddenSupervisors renews before the blocking scan.\n\nThe 4603 regression drops its 11s sleep for a post-shutdown listener assertion\nand uses mkdtemp for the child TMPDIR.\n\n* delete 148 dead test files\n\nRemoves test files judged CUT by a full 480-file audit: duplicates of a named survivor, mock-asserting tests, rendered-copy assertions, private-internals casts, and live-API matrices behind skipIf(!API_KEY) that never ran in CI.\n\n* merge interactive-mode and agents-view test suites\n\n* merge tui render, overlay and helper test suites\n\n* test: slim daemon-mode and agent-connection-daemon suites\n\ndaemon-mode: keep passive hydration/passivation, delete_subagent durability, agent-message concurrency and rate limits, attach-snapshot backpressure; drop per-race internal regressions already covered process-level by daemon-supervisor-process.\nagent-connection-daemon: keep reconnect, update-restart, chunked-snapshot assembly and disposal races; drop tautological daemon-protocol pass-through cases.\n\n* merge tui editor, markdown and autocomplete test suites\n\n* test: merge audited session-core test bloat\n\nExecute MERGE verdicts from the 480-file test audit for the agent-session\ncore suites: collapse near-identical blocks into it.each tables, drop\nmock-return/rendered-copy assertions and private-cast white-box cases,\nand fold one-off files into the owning suite.\n\nDeleted (folded into their owning suite):\n- agent-session-concurrent.test.ts, suite/agent-session-action-races.test.ts,\n  suite/agent-session-action-contracts.test.ts -> suite/agent-session-queue.test.ts\n- suite/agent-session-compaction-continuation.test.ts,\n  suite/agent-session-compact-skill.test.ts -> suite/agent-session-compaction.test.ts\n- agent-session-runtime-events.test.ts -> suite/agent-session-runtime.test.ts\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency/ordering, crash/restart/resume and load coverage.\n\n* address review: octal probe timeout, truncated deadline, dead schedule field\n\n* test: merge redundant coding-agent test cases into case tables\n\nExecutes the MERGE verdicts from the 480-file test-bloat audit for the\nmisc lanes. Repeated near-identical blocks collapse into single it.each\ncase tables, and ticket-numbered one-off files fold into the owning\nmodule suite.\n\nRemoved throughout the touched files: assertions on mock return values,\nassertions on rendered text/copy, `as unknown as` casts into privates,\nand suites gated behind skipIf(!API_KEY) (which never ran in CI).\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency and ordering, crash/restart/resume, and load.\n\nDeleted files, folded into their module suite:\n- compaction-extensions -> deleted outright (whole file behind skipIf(!API_KEY))\n- compaction-serialization -> compaction\n- heartbeat-catalog -> daemon-client\n- child-process -> exec\n- edit-tool-legacy-input -> tools\n- kernel-goal-skill, kernel-rlm-heartbeat-skill -> kernel-agent-message-skill\n- kernel-agent-observe-skill -> kernel-attach-image-skill\n- kernel-windows-process -> kernel-bootstrap-windows\n- extensions-input-event -> extensions-runner\n- sdk-skills -> sdk-session-manager\n- session-manager/{agent-status,has-user-content} -> session-manager/session-state\n- session-manager/migration -> session-manager/file-operations\n- session-manager/custom-session-id -> session-manager/tree-traversal\n\n* slim ai + regression tests (lanes A/B/D/E)\n\nExecute MERGE verdicts from the test audit for packages/ai and the\nupdate-restart / snapshot / session-view regression files.\n\n- fold ticket-numbered regression cases into the owning suites\n- collapse near-identical blocks into it.each tables\n- drop mock-return, rendered-copy and live-API-gated assertions\n\n* test: collapse package-manager tables and fold small daemon selector suites\n\npackage-manager: git-URL parsing, top-level/manifest/package glob filters and npm install-argv cases are now table-driven.\ndaemon-session-id: absorbs daemon-catalog-process selector case, drops display-id formatting trivia.\ndaemon-worker-windows-timeouts: absorbs the connection-deadline budget cases from daemon-worker-connect.\ndaemon-catalog-startup: drops the src-directory one-off.\n\n* test: collapse refinement, settings and config suites into case tables\n\nFinal misc lane of the test-bloat audit.\n\n- refinement: per-kind create/update/delete becomes one it.each lifecycle;\n  8 rejection suites become one 18-row invalid-edit table that also asserts\n  state and the refinement log stay untouched; truncation/parse errors,\n  scope permutations, ranking and the tokenizer each become one table.\n  System-prompt and user-prompt copy assertions removed.\n- settings-manager: per-setting default getters removed; unknown-key\n  preservation, invalid-file guards and project-vs-global precedence kept.\n- config: install-method detection becomes one table; session-dir env\n  permutations collapse into one precedence test.\n- builtin-skills, repl-kernel-abort, session-cwd, truncate: repeated blocks\n  collapsed; one ReplInternals reach-in dropped.\n- child-process folds into exec as one process lifecycle suite.\n\n* slim regression tests (lane C)\n\nFold the remaining ticket-numbered regression files into the owning\nmodule suites and collapse duplicate cases into it.each tables.\n\n- 19 regression files deleted after their kept cases moved into\n  agent-session-queue, auth-storage, acp-features, resource-loader,\n  agent-session-runtime/compaction/retry-events/recursion,\n  model-registry, slash-commands, daemon-supervisor-process\n- 3 files slimmed in place where the named target no longer exists\n- drop rendered-copy, mock-return and private-cast assertions\n\n* test: collapse daemon protocol, command, list, ps and package-command suites\n\ndaemon-protocol: two gating tables replace 16 near-identical capability/schema cases.\ndaemon-command: table-driven argv parsing plus the --goal leak guards; drops help/usage copy assertions.\ndaemon-session-list: activity, recap and model-fallback projections are table-driven.\ndaemon-ps: keeps planReap/planShutdownAll safety rules, drops ss/lsof parsers, sorting and uptime trivia.\ndaemon-stop-confirm: one busy-guard table, no prompt copy.\npackage-command-paths: one shared self-update fixture for the install-spec cases; drops help and removed-flag copy.\nacp-events: keeps only the cases the ACP suite does not cover end to end.\ndaemon-session-summarizer: one recap/status parse table.\ndaemon-extension-binding: keeps the duplicated-partial-message strip.\n\n* test: tighten the daemon stop-confirm busy-guard table\n\n* fix ci: biome forEach body, vacuous autocomplete case, missing prompt before second compaction\n\n* fix ci: restore escaped template literals in prompt-template cases\n\n* fix codeql: single-pass path escaping in test helpers\n\n* address review: flush debounced trace uploads, restore probe timeout guards, drop dead test scaffolding\n\n* respect retry-after and throttle window when flushing trace uploads\n\n* await in-flight trace uploads before a drain returns\n\n* extend deterministic test policy to python\n\n* harden deterministic test policy checks\n\n* close test policy edge cases\n\n* finish deterministic policy edge coverage\n\n* fix policy lexer and macos signature setup\n\n* mask regex literals in test policy checks\n\n* distinguish postfix division in policy lexer\n\n* close native migration signal race\n\n* keep native migration force timer owned\n\n---------\n\nCo-authored-by: prime-agent <agent@local>"},{"name":"flaky-supervisor-monitor.md","kind":"file","size":163,"lastCommitHash":"cf07c5a3f5eca98e7744f2df83050044c920252a","lastCommitWhen":"2026-09-16T16:28:13-04:00","lastCommitMessage":"delete dead tests and fix the races the flaky ones were hiding (#2336)\n\n* add an objective testing policy to AGENTS.md\n\n* fix flaky daemon supervisor monitor test\n\nThe supervisor availability check is a real async chain (registry stat,\nsocket probe, replacement launch) started from a timer callback, so the\ntest drove a bounded fake-timer pump and asserted on a probe counter.\nUnder a loaded CI shard the chain did not finish inside the iteration\nbudget, and the variant mixing fake timers with a real probe promise\ncould deadlock for the full 30s test timeout.\n\nExpose a settled promise per armed availability check and await it in\nthe tests, so each assertion runs after the check finished instead of\nafter an arbitrary number of clock advances.\n\n* remove vitest retry masking from ai and attach-image tests\n\nDeleted all 548 `retry:` options in packages/ai/test and the 2 in\nkernel-attach-image-skill.test.ts. Retries hid non-determinism instead of\nfixing it, so a broken code path could still go green.\n\n- Deleted 17 live-only cross-provider E2E matrices whose every case sits behind\n  skipIf(!API_KEY) and never runs in CI; stream.test.ts stays as the survivor\n  that owns the provider table.\n- Stripped the retry option from the 198 stream.test.ts cases and dropped the\n  live E2E describe (plus its now-unused helpers) from\n  anthropic-thinking-disable.test.ts, keeping its mocked payload contract.\n- Removed retry: 1 from the two attach-image kernel cases; they pass without it.\n\nMocked wire/SSE contract tests are untouched. Tests only, no product code.\n\n* make agent trace upload tests deterministic\n\nThe suite pumped a synthetic clock with advanceTimersUntil() and hoped real\nfs and promise work settled inside a 200-step budget, asserted schedule\ndelays through a global setTimeout spy whose fake clock had drifted, and\nremoved its temp dir while catch-up and cursor writers were still running.\nUnder a loaded CI shard that produced \"Timed out advancing fake timers\",\n\"expected 1000 to be 60000\" and \"ENOTEMPTY rmdir /tmp/agent-traces-test-*\".\n\nThe upload scheduler now reports the delay it arms and the outcome of each\ncycle, the upload reports every wait it arms before the next request, and\ninstallAgentTraceUpload returns a handle that resolves when the startup\ncatch-up and any in-flight upload have finished writing. Tests await those\nsignals and advance the fake clock by the exact reported delay, and teardown\nwaits for in-flight writers before removing the temp dir.\n\nThe ~15 retry, backoff and Retry-After variants collapse into one\ntable-driven it.each, per the MERGE verdict: 1569 -> 1141 lines, 44 -> 27\ntest blocks, with no lost coverage.\n\n* make the compiled artifact and installer probe tests deterministic\n\nThe compiled-artifact afterEach polled for supervisor exit with the same\n10s budget as vitest's hook timeout, so the poll could never report on the\nslower standalone runners. The teardown now escalates to SIGKILL and every\ninner deadline stays below the surrounding hook and test budgets.\n\nnative-probe-timeout decided pass/fail from wall-clock deadlines around a\nreal 10s installer probe. The installer probe deadline is now injectable\nthrough PRIME_AGENT_PROBE_TIMEOUT_SECONDS, the blocked probe records the\ninstall lock itself, and the spawn deadlines are gone.\n\n* fix daemon shutdown admission loss during slow scans\n\nshutdown --force holds a 5s admission lease refreshed by a 1s timer, but it\nblocks its own event loop in synchronous ps/lsof/ss scans. On a loaded runner a\nscan outlasts the lease, the elapsed record is treated as abandoned, and the\nholder's next assertOrRenew throws \"Daemon shutdown admission was lost\".\n\n- A late renew re-arms a record that is still ours; only a missing or foreign\n  record ends the admission.\n- A renew that could not read the registry no longer retires the lease.\n- isDaemonShutdownAdmissionActive is read-only, so a bystander probe cannot\n  delete a live holder's record.\n- stopHiddenSupervisors renews before the blocking scan.\n\nThe 4603 regression drops its 11s sleep for a post-shutdown listener assertion\nand uses mkdtemp for the child TMPDIR.\n\n* delete 148 dead test files\n\nRemoves test files judged CUT by a full 480-file audit: duplicates of a named survivor, mock-asserting tests, rendered-copy assertions, private-internals casts, and live-API matrices behind skipIf(!API_KEY) that never ran in CI.\n\n* merge interactive-mode and agents-view test suites\n\n* merge tui render, overlay and helper test suites\n\n* test: slim daemon-mode and agent-connection-daemon suites\n\ndaemon-mode: keep passive hydration/passivation, delete_subagent durability, agent-message concurrency and rate limits, attach-snapshot backpressure; drop per-race internal regressions already covered process-level by daemon-supervisor-process.\nagent-connection-daemon: keep reconnect, update-restart, chunked-snapshot assembly and disposal races; drop tautological daemon-protocol pass-through cases.\n\n* merge tui editor, markdown and autocomplete test suites\n\n* test: merge audited session-core test bloat\n\nExecute MERGE verdicts from the 480-file test audit for the agent-session\ncore suites: collapse near-identical blocks into it.each tables, drop\nmock-return/rendered-copy assertions and private-cast white-box cases,\nand fold one-off files into the owning suite.\n\nDeleted (folded into their owning suite):\n- agent-session-concurrent.test.ts, suite/agent-session-action-races.test.ts,\n  suite/agent-session-action-contracts.test.ts -> suite/agent-session-queue.test.ts\n- suite/agent-session-compaction-continuation.test.ts,\n  suite/agent-session-compact-skill.test.ts -> suite/agent-session-compaction.test.ts\n- agent-session-runtime-events.test.ts -> suite/agent-session-runtime.test.ts\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency/ordering, crash/restart/resume and load coverage.\n\n* address review: octal probe timeout, truncated deadline, dead schedule field\n\n* test: merge redundant coding-agent test cases into case tables\n\nExecutes the MERGE verdicts from the 480-file test-bloat audit for the\nmisc lanes. Repeated near-identical blocks collapse into single it.each\ncase tables, and ticket-numbered one-off files fold into the owning\nmodule suite.\n\nRemoved throughout the touched files: assertions on mock return values,\nassertions on rendered text/copy, `as unknown as` casts into privates,\nand suites gated behind skipIf(!API_KEY) (which never ran in CI).\n\nPreserved: process boundaries (daemon RPC, ACP), on-disk formats,\nconcurrency and ordering, crash/restart/resume, and load.\n\nDeleted files, folded into their module suite:\n- compaction-extensions -> deleted outright (whole file behind skipIf(!API_KEY))\n- compaction-serialization -> compaction\n- heartbeat-catalog -> daemon-client\n- child-process -> exec\n- edit-tool-legacy-input -> tools\n- kernel-goal-skill, kernel-rlm-heartbeat-skill -> kernel-agent-message-skill\n- kernel-agent-observe-skill -> kernel-attach-image-skill\n- kernel-windows-process -> kernel-bootstrap-windows\n- extensions-input-event -> extensions-runner\n- sdk-skills -> sdk-session-manager\n- session-manager/{agent-status,has-user-content} -> session-manager/session-state\n- session-manager/migration -> session-manager/file-operations\n- session-manager/custom-session-id -> session-manager/tree-traversal\n\n* slim ai + regression tests (lanes A/B/D/E)\n\nExecute MERGE verdicts from the test audit for packages/ai and the\nupdate-restart / snapshot / session-view regression files.\n\n- fold ticket-numbered regression cases into the owning suites\n- collapse near-identical blocks into it.each tables\n- drop mock-return, rendered-copy and live-API-gated assertions\n\n* test: collapse package-manager tables and fold small daemon selector suites\n\npackage-manager: git-URL parsing, top-level/manifest/package glob filters and npm install-argv cases are now table-driven.\ndaemon-session-id: absorbs daemon-catalog-process selector case, drops display-id formatting trivia.\ndaemon-worker-windows-timeouts: absorbs the connection-deadline budget cases from daemon-worker-connect.\ndaemon-catalog-startup: drops the src-directory one-off.\n\n* test: collapse refinement, settings and config suites into case tables\n\nFinal misc lane of the test-bloat audit.\n\n- refinement: per-kind create/update/delete becomes one it.each lifecycle;\n  8 rejection suites become one 18-row invalid-edit table that also asserts\n  state and the refinement log stay untouched; truncation/parse errors,\n  scope permutations, ranking and the tokenizer each become one table.\n  System-prompt and user-prompt copy assertions removed.\n- settings-manager: per-setting default getters removed; unknown-key\n  preservation, invalid-file guards and project-vs-global precedence kept.\n- config: install-method detection becomes one table; session-dir env\n  permutations collapse into one precedence test.\n- builtin-skills, repl-kernel-abort, session-cwd, truncate: repeated blocks\n  collapsed; one ReplInternals reach-in dropped.\n- child-process folds into exec as one process lifecycle suite.\n\n* slim regression tests (lane C)\n\nFold the remaining ticket-numbered regression files into the owning\nmodule suites and collapse duplicate cases into it.each tables.\n\n- 19 regression files deleted after their kept cases moved into\n  agent-session-queue, auth-storage, acp-features, resource-loader,\n  agent-session-runtime/compaction/retry-events/recursion,\n  model-registry, slash-commands, daemon-supervisor-process\n- 3 files slimmed in place where the named target no longer exists\n- drop rendered-copy, mock-return and private-cast assertions\n\n* test: collapse daemon protocol, command, list, ps and package-command suites\n\ndaemon-protocol: two gating tables replace 16 near-identical capability/schema cases.\ndaemon-command: table-driven argv parsing plus the --goal leak guards; drops help/usage copy assertions.\ndaemon-session-list: activity, recap and model-fallback projections are table-driven.\ndaemon-ps: keeps planReap/planShutdownAll safety rules, drops ss/lsof parsers, sorting and uptime trivia.\ndaemon-stop-confirm: one busy-guard table, no prompt copy.\npackage-command-paths: one shared self-update fixture for the install-spec cases; drops help and removed-flag copy.\nacp-events: keeps only the cases the ACP suite does not cover end to end.\ndaemon-session-summarizer: one recap/status parse table.\ndaemon-extension-binding: keeps the duplicated-partial-message strip.\n\n* test: tighten the daemon stop-confirm busy-guard table\n\n* fix ci: biome forEach body, vacuous autocomplete case, missing prompt before second compaction\n\n* fix ci: restore escaped template literals in prompt-template cases\n\n* fix codeql: single-pass path escaping in test helpers\n\n* address review: flush debounced trace uploads, restore probe timeout guards, drop dead test scaffolding\n\n* respect retry-after and throttle window when flushing trace uploads\n\n* await in-flight trace uploads before a drain returns\n\n* extend deterministic test policy to python\n\n* harden deterministic test policy checks\n\n* close test policy edge cases\n\n* finish deterministic policy edge coverage\n\n* fix policy lexer and macos signature setup\n\n* mask regex literals in test policy checks\n\n* distinguish postfix division in policy lexer\n\n* close native migration signal race\n\n* keep native migration force timer owned\n\n---------\n\nCo-authored-by: prime-agent <agent@local>"},{"name":"google-antigravity-extension.md","kind":"file","size":113,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"gpt-6-astra-codex-version.md","kind":"file","size":102,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"guard-find-ls-commands.md","kind":"file","size":335,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"guard-nix-and-skill-escape-hatches.md","kind":"file","size":127,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"harness-hygiene.md","kind":"file","size":385,"lastCommitHash":"348df40d4f355e1b1774dcb5c64c6eb05c8040ec","lastCommitWhen":"2026-09-17T13:19:59+03:00","lastCommitMessage":"feat(coding-agent): curate restored state, frame heartbeats, and add violation counters\n"},{"name":"heartbeat-catalog-first-paint.md","kind":"file","size":161,"lastCommitHash":"1ed762a069143aaa94dbaa4a8e287e0c83a46910","lastCommitWhen":"2026-09-17T06:42:11+03:00","lastCommitMessage":"coding-agent/modes/interactive: paint the session before the heartbeat catalog\n"},{"name":"hide-goal-accounting.md","kind":"file","size":113,"lastCommitHash":"6e1c74a85086204e714665a3a51adee0c23b8fc0","lastCommitWhen":"2026-09-17T09:21:12+03:00","lastCommitMessage":"fix(coding-agent): hide model-facing goal accounting metadata\n"},{"name":"hide-model-context-metadata.md","kind":"file","size":115,"lastCommitHash":"065d4423ccd3c986fdf72a50d86321fbc363adb0","lastCommitWhen":"2026-09-17T08:52:00+03:00","lastCommitMessage":"Remove model-facing context metadata\n"},{"name":"interrupted-tool-call-result.md","kind":"file","size":157,"lastCommitHash":"a94490eda202a79bb524e9822bb6c8bf6f3f04ea","lastCommitWhen":"2026-09-18T00:07:24+03:00","lastCommitMessage":"coding-agent: answer the tool call a replaced session left unresolved\n"},{"name":"jj-log-template-list.md","kind":"file","size":109,"lastCommitHash":"d4a02ed68767728a6fb2cd8e3d8b3e801909136c","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"packages/coding-agent/skills/jj: return TemplateList for templated log queries\n"},{"name":"kernel-cwd-survives-resume.md","kind":"file","size":132,"lastCommitHash":"852221000f1e3eb4966d0b6fd08d2b45044ae56e","lastCommitWhen":"2026-09-17T07:06:13+03:00","lastCommitMessage":"fix(kernel): keep the working directory as session state, not as snapshot state\n\nA cell's `os.chdir` was lost on resume while the variables it was used with survived, so\na later relative path silently resolved somewhere else — in this session it put a\nread-only `git status` in the wrong repo. The first attempt recorded the directory in the\nsnapshot manifest, which did not hold: the manifest is variable state, written and\nre-read more than once per handoff, and the directory came back wrong.\n\nThe directory is now its own session artifact (`kernel-cwd`), written once when the\nkernel reports where it stood and read once at boot, so no ordering in the handoff can\nlose or overwrite it:\n\n- `snapshot` returns `cwd` in its result; the host writes it beside the session's other\n  state. The manifest no longer carries it.\n- `ReplKernelManager.start()` resolves `readKernelCwd(...)` before spawning and spawns\n  the kernel there, so the process starts in the right place instead of being moved\n  afterwards — the first import and the first relative path see it too.\n- A record whose directory is gone is never used: it is reported as `cwdMissing`, and the\n  notice says the kernel is somewhere else now. A relative or empty record is ignored.\n- `restore` is back to reviving the namespace only.\n\nVerified: runtime `test_repl` 120 tests (including the snapshot result and the\nnamespace-only restore), host tests for the record round-trip, the gone directory, and\nthe relative/empty cases, plus the existing notice test; negative control with the\nreported `cwd` removed fails the new runtime test as it should.\n"},{"name":"kernel-venv-atomic-build.md","kind":"file","size":391,"lastCommitHash":"92214706071e3745ccd043e0ace814ba7452024e","lastCommitWhen":"2026-09-17T14:10:42+03:00","lastCommitMessage":"fix(coding-agent): make the kernel venv rebuild atomic and the kernel runtime self-healing\n"},{"name":"kpi-reliability-hardening.md","kind":"file","size":449,"lastCommitHash":"228fafa16c5f360ebcb9d68584ae38b5bef922bf","lastCommitWhen":"2026-09-17T06:33:08+03:00","lastCommitMessage":"fix(agent): harden reliability failure paths\n"},{"name":"language-drift-notice-at-turn-end.md","kind":"file","size":135,"lastCommitHash":"6d2a1eaac18ce638199a08733d20b8fdfc99ad98","lastCommitWhen":"2026-09-17T09:06:06+03:00","lastCommitMessage":"Deliver the language-drift notice at turn end\n"},{"name":"live-context-usage.md","kind":"file","size":92,"lastCommitHash":"c31b4f5386a5db7b94f10669695041fd2c1b438d","lastCommitWhen":"2026-09-17T06:20:57+03:00","lastCommitMessage":"coding-agent/{src/modes/interactive,test}: update live context usage\n"},{"name":"live-models-dev-reloading.md","kind":"file","size":104,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"mcp-accounts-menu-three-rows-and-back.md","kind":"file","size":528,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"mcp-connection-outcome-messages.md","kind":"file","size":429,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"mcp-paste-outcome-and-menu-polish.md","kind":"file","size":221,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"mcp-picker-flatten-catalog-copy.md","kind":"file","size":173,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"mcp-picker-live-testing-nits.md","kind":"file","size":1129,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"mcp-picker-one-line-detail-and-stable-render.md","kind":"file","size":541,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"mcp-picker-stays-open-after-action.md","kind":"file","size":471,"lastCommitHash":"b6ac5d014d99401b55820835a4966584271e9a3c","lastCommitWhen":"2026-09-16T16:20:18-07:00","lastCommitMessage":"mcp service catalog and oauth engine (#2330)\n\n* add a shared service catalog and generic mcp connections (#2256)\n\n* style(coding-agent): apply biome formatting to daemon-mode.ts\n\n* feat(coding-agent): add /plugins service catalog with verified MCP connections (ENG-6108)\n\n- Add /plugins and bare /mcp as the same searchable external-service picker with\n  honest states (Connect, Connected with verified tool counts, Reconnect,\n  Verifying, Requires setup, Disabled) and connect/disconnect actions\n- Add mcp.list_plugins, mcp.search_plugins, and mcp.list_connections host\n  requests with strict status filters, bounded limits, and honest nextCursor\n- Add local connection records (connectionId distinct from catalog serviceId);\n  verify every login with a real MCP handshake so token presence alone is\n  pending, never Connected; rejected credentials recover via Reconnect\n- Serve connected catalog services (Linear, Notion) through the generic mcp\n  runtime route: system-prompt inventory and mcp.config, no per-service wrappers\n- Drop the McpIntegration import from the kernel ready check ahead of the\n  Linear/Notion wrapper-package removal\n- mcp.connect is registered only behind an explicit UI-approved callback and\n  stays unregistered in daemon-hosted sessions (honest \"no handler\")\n\nTests: mcp-service-catalog, mcp-manager handlers, service-catalog-picker, and\nthe eng-6108 activation regression covering login -> verify -> session reload ->\nprompt inventory in the real services path without a restart.\n\n* fix(coding-agent): harden MCP verification, activation, and connection records (ENG-6108)\n\nReview follow-ups on the /plugins service catalog slice:\n\n- Verification now runs through the official @modelcontextprotocol/sdk\n  client (negotiated protocol version header, CRLF SSE support, explicit\n  session termination via HTTP DELETE) instead of a handwritten transport\n- Failures persist as fixed safe categories only; endpoint URLs,\n  server-controlled text, and tokens never reach records or messages\n- Connection records use the auth.json file-lock read-modify-write pattern\n  with unique temp files; concurrent UI/daemon writers no longer lose\n  records, and pending operations queue while a flush is in flight\n- Every verification result is bound to the exact grant it verified\n  (credential hash); stale probes after logout or token rotation are\n  discarded, never marking a newer or removed account Connected\n- An OAuth connection without a usable token is never probed anonymously\n- A login made while streaming or compacting queues automatic activation\n  at the next safe boundary (agent end / compaction end); the UI never\n  instructs the user to run /reload for MCP changes\n- /plugins and config-menu connect paths share the queue; failure categories\n  render as fixed user-readable wording\n- metadata-reviewed / legacyBuiltin rename absorbed from the catalog lane\n\nTests: probe (SDK fake server incl. CRLF/401/timeout/secret-leak), store\n(two-writer races, guard discard, mid-flight queueing), service catalog\n(stale verify after logout/rotation, non-connectable transports), manager,\nactivation queue (streaming/compaction/tool boundaries), regression suite.\n\n* fix(coding-agent): bind MCP credentials to endpoints, reconcile providers, honest connect (ENG-6108)\n\n- Endpoint binding everywhere a stored grant is consulted: catalog services and\n  user-declared servers require cred.endpoint === current URL before any token\n  fetch or probe (views, verify, dispatch inventory, demand-driven listing);\n  unbound legacy grants surface as Reconnect with the fixed credential-unbound\n  category and are recorded once on demand-driven verification\n- Provider ownership is one atomic reconciliation: legacy built-ins, eligible\n  catalog services, and user-declared OAuth servers are re-registered first and\n  only previously-owned ids unregister; the ModelRegistry reset hook calls the\n  full reconciliation, so catalog providers survive refresh and a removed user\n  override restores the catalog provider after a single manager.refresh()\n- Flush-time verification guards re-read fresh credential-source state\n  (authStorage.reload() under the backend lock), so probes that finish after\n  another client logged out or rotated the grant are discarded — no stale\n  Connected record survives across clients\n- mcp.connect (explicit-approver seam) verifies the real handshake before\n  reporting connected (tool count included) and reports fixed failure\n  categories on rejected handshakes\n- /plugins stdio user servers get a real management action: Enter disables the\n  server in global settings, refreshes providers, and reloads; already-disabled\n  and vanished entries report honest status\n- Catalog views keep the Reconnect action on error rows and add unverified/\n  unbound view tests; all oauth test fixtures now carry bound endpoints\n\nMinimal type-only addition in connection-probe.ts (lifecycle lane owns the file):\nMCP_PROBE_ERRORS.UNBOUND_CREDENTIAL.\n\nTests: manager 29, service-catalog 23, activation-queue 8 (incl. stdio\nmanagement), probe 7, store 6, picker 4, regression 2, slash-commands 18 —\n97/97; reviewer's mcp-service-safety suite verified 12/12 green against this\ntree (their file, their branch).\n\n* feat(ai): merged MCP service catalog with deterministic importer (ENG-6108)\n\n* feat(ai): metadata-reviewed catalog status, legacy builtins, local JSON sources, literal endpoint checks (ENG-6108)\n\n* fix(ai): harden local catalog loader and endpoint checks (ENG-6108)\n\n* feat(coding-agent): shared catalog resolver, local sources, per-account connections (ENG-6108)\n\n- resolveMcpServiceCatalog merges the built-in SERVICE_CATALOG with\n  settings-declared local sources (Settings.mcpCatalogSources): ~-expanded\n  before loadLocalServiceCatalog (which does no expansion), declared-but-missing\n  files and cross-source duplicate ids surface as visible diagnostics, first\n  source wins, total capped at 500 entries\n- The SAME resolution feeds the /plugins UI and the mcp host handlers; the UI\n  re-reads connection records from disk before rendering (no stale cache) and\n  shows resolver diagnostics on open\n- One-click Connect is metadata-reviewed-only (plus user-placed local files,\n  which still go through the login dialog's explicit approval); unreviewed\n  imported entries stay candidates with an honest setup hint\n- Per-account connections: \"Add another account\" allocates a distinct\n  connectionId (mcp:<serviceId>-N) with its own credential key, record, and\n  OAuth provider; the account picker lists accounts with per-account\n  Reconnect/Disconnect; alias ids join integrations, dispatch inventory,\n  mcp.config, and demand-driven verification through the same machinery; the\n  record's serviceId keeps the parent catalog id\n- test/mcp-service-catalog.test.ts settles the theme's cli-highlight preload\n  in beforeAll (deterministic fix for the pre-existing initTheme fire-and-forget\n  teardown race, evidence in ENG-6108 notes); ServiceCatalogPickerComponent\n  gains title/subtitle/hideSearch options for the account listing\n\nCatalog commits f8a4cf61a -> 50b79c3af -> be7445c3c integrated via authorized\nlocal cherry-picks; no catalog-source files modified.\n\nTests: manager 31 (alias integrations/provider lifecycle, demand-driven unbound\nrecord, reconciliation), service-catalog 29 (resolver, candidates, allocator),\nactivation-queue 9 (stdio management, add-account), probe 7, store 6, picker 4,\nregression 2, slash-commands 18 — 105/105; npm run check EXIT=0; 10/10\nconsecutive scoped runs with zero unhandled rejections, exit 0 each.\n\n* mcp: generic discovery surface, drop linear/notion wrappers (ENG-6108)\n\nAdd a bounded discovery surface to the kernel MCP module:\nlist_plugins, search_plugins, list_connections, search_tools and\ndescribe_tool, backed by host-owned catalog/connection handlers,\nwith cursor-safe tools/list pagination, explicit search scopes and\ndefensive secret scrubbing of inventory metadata only.\n\nReplace the bespoke Linear/Notion Python wrapper packages with one\ngeneric bundled mcp skill; remove the McpIntegration base class after\nauditing consumers, moving its shared helpers into rlm.mcp. Connecting\na service is a user action (/plugins or /mcp login), never an install\nor a per-service Python module.\n\n* mcp: fail-closed tool discovery, isolated schemas, redacted inventory errors (ENG-6108)\n\nFollow-up to 6514cbe6f per review:\n\n- discover() now publishes a tool map only when tools/list pagination\n  completes; a repeated or malformed continuation cursor, or exceeding\n  the page cap, raises McpDiscoveryError instead of a silent partial\n  inventory. Malformed cursors are rejected rather than read as done.\n- list_tools/describe_tool return deep copies, so caller mutations can\n  never reach the cached canonical schemas.\n- Per-connection search failures report a fixed exception type + hint\n  (McpCredentialsUnavailable and friends) instead of raw exception text;\n  host_request error text echoed by inventory calls is scrubbed of\n  credential-bearing URLs, query strings and auth headers and bounded.\n\n* mcp: never chain raw host errors in inventory wrappers (ENG-6108)\n\nFollow-up to ff738187e per review. _host_inventory raises a fixed\n\"MCP <request-type> request failed/timed out\" wrapper outside the\nexception handler, so arbitrary host/bridge error text (which can\nembed credential-bearing URLs, query strings, headers and HTTP\nbodies) is never echoed and cannot be reached through __cause__ or\n__context__ either; format_exception prints stay secret-free. The\nnow-unused redaction regex helper and bound constant are removed.\n\nRegression uses traceback.format_exception, not only str(exc), to\nassert the original exception is fully absent. Skill updated for the\nhost status vocabulary (pending, verified handshake) and PluginView\nfields (setupHint, unverified, verifiedAt/toolCount, connectionIds).\n\n* fix(coding-agent): bound MCP probe lifetime and connection-store races (ENG-6108)\n\nRoot review fixes on the lifecycle lane:\n\n- probeMcpEndpoint: the SDK passes its own controller's signal to every\n  fetch (including the cleanup DELETE and its body cancel), and the SDK\n  request timeouts only race the JSON-RPC promise, so an unresponsive\n  DELETE could hang the probe forever. One lifecycle AbortController now\n  bounds the probe: a fetch wrapper merges its signal into every\n  transport fetch, terminateSession/close are each raced against a\n  cleanup grace deadline whose expiry aborts the signal, an absolute\n  deadline timer plus a final abort end lingering body reads, and every\n  timer is cleared. Failure categories stay fixed and safe.\n- McpConnectionStore: the pre-lock exists-then-write-empty create could\n  rename an empty file over records another process just wrote; the file\n  is now created exclusively (wx, never truncating) before the canonical\n  realpath lock, with mode 0600. A failed atomic write no longer drops\n  queued mutations: the batch is unshifted back to the front of the\n  queue and retried by the next flush (ops are idempotent; verify\n  guards re-evaluate under the lock).\n- MCP_PROBE_ERRORS gains UNBOUND_CREDENTIAL (\"credential-unbound\") for\n  grants not bound to the endpoint, consumed by the service catalog.\n\nOffline regressions: never-returning cleanup DELETE, hanging response\nbody, exclusive first-create race with mode 0600, injected atomic-write\nfailure with in-order retry and stale-guard discard, plus the existing\nsuites. All fetch stubs; no live credentials or provider calls.\n\n* test(coding-agent): multi-PID first-create regression for MCP connection store (ENG-6108)\n\nRoot review follow-up: in-process instances cannot reproduce the\nfirst-create exists/write race because their pre-await work is\nserialized on one event loop. The store test file now also spawns four\nreal first-writer processes (temporary .mts worker imported through the\nproject tsx loader, no production entry point), aligned by ready\nmarkers + a file barrier, racing the first create and the locked flush\non the shared store path. Asserts distinct PIDs, all children exit\ncleanly, every record survives, and the file keeps mode 0600; children\nare terminated in a finally on every path.\n\nTest-only: no production files touched.\n\n* test(coding-agent): Windows-portable multi-PID store regression (ENG-6108)\n\nTest-only portability follow-up: the generated first-create worker\nimported the store module by resolved absolute OS path, which is not a\nvalid ESM specifier on Windows; it now imports by file URL, and the\nexact 0600 mode assertions are POSIX-only (Windows mode bits do not\nmap meaningfully). No production changes.\n\n* test(coding-agent): add service-safety adversarial regressions for MCP credentials and providers (ENG-6108)\n\nAdversarial regression suite for the ENG-6108 service-catalog safety\ncontract, red on the 4bd8abc12 baseline and green on the wave-2 fixes:\n\n- Credential binding before probe/token access: a stored credential bound\n  to a different endpoint, or unbound, is rejected before any probe call\n  and before authStorage.getApiKey - for user-declared servers and\n  catalog services, through direct verifyConnection and the demand-driven\n  mcp.list_plugins background verification; never a connected record.\n- Cross-client stale probes: two AuthStorage instances share one\n  auth.json; a probe finishing after a second client logs the grant out\n  or replaces it mid-flight must not persist a connected record or\n  verifiedAt for the current grant.\n- OAuth provider lifecycle: a full OAuth registry reset (through the\n  real createAgentSessionServices + modelRegistry.refresh seam)\n  preserves non-bundled catalog providers, and removing a user override\n  restores the catalog provider after one refresh.\n\nOffline-only: injected probes, denied fetch, per-test temp credential\nand connection stores, no live endpoints or real credentials.\n\n* fix(coding-agent): durable account reservations, per-account views, honest actions (ENG-6108)\n\nFinal-review wave 4:\n\n- Account allocation is a durable reservation under the connection-store file\n  lock: reserveConnectionId applies atomically on disk (exactly one winner in\n  a cross-process race; the loser allocates the next id), pending markers are\n  visible to every client, and removeReservation is ownership-validated (id +\n  still-pending + same createdAt) so a late callback never removes or resurrects\n  another client's account. Allocation consults store records, credentials,\n  configured user servers, and catalog ids; configured names are never taken.\n- Views aggregate every account of a service: connectionIds lists all accounts,\n  each account's status reflects its own bound credential, the service card\n  survives on a remaining account after the default disconnects, and the\n  inventory emits one row per account (no phantom primary row). Alias account\n  ids are searchable (search_plugins alias contract). Connections whose local\n  catalog source vanished keep a pinned descriptor at the recorded endpoint —\n  credentials are never retargeted.\n- Explicit state actions: pending accounts retry verification without a\n  relogin (the \"retry from /plugins\" path works), error accounts keep\n  Reconnect (catalog and user rows), each account gets an explicit Remove\n  row, and a verification result that could not be saved reports pending —\n  never a false Connected.\n- resolveMcpServiceCatalog isolates local-source failures per source: one\n  unreadable or invalid file becomes a bounded visible diagnostic while the\n  built-in catalog and every other source keep resolving.\n\nTests: two-client simultaneous reservation race with barrier, ownership\nvalidation, full add-second -> reload -> inventory -> disconnect-default ->\nsecond-still-manageable regression, per-account view aggregation, vanished-\nsource pinning, per-source failure isolation, state-based action tests, and\nallocation that skips configured ids.\n\nOwnership: bootstrap.ts untouched (delegated lane).\n\n* fix(coding-agent): require MCP discovery methods in the kernel ready check (ENG-6108)\n\nThe runtime ready check only asserted mcp.list_tools/mcp.call_tool; the\nfive new discovery methods were validated once by the bootstrap import\nscan, so a cached kernel venv with an old prime-agent-runtime could\npass readiness. The check now requires callables for list_plugins,\nsearch_plugins, list_connections, search_tools and describe_tool, and\nfails with an actionable message so the venv is rebuilt instead of\nsilently reused.\n\nRUNTIME_READY_CHECK is exported so the new regression executes the\nexact shipped check against a real project runtime: it accepts the\ncurrent runtime, and rejects a runtime simulated as old by deleting\neach discovery method in-process (not a text-grep test). No provider\ncalls, credentials, or real daemon involved.\n\n* fix(coding-agent): guarded account credential commits and computed account status (ENG-6108)\n\nFinal-review wave 5:\n\n- Reservations resolve only after the durable record write COMMITS: the\n  store defers reserve/remove/finalize callbacks past writeFileAtomicSync,\n  resolves them false on write failure, and drops the one-shot ops instead\n  of requeueing them — a failed write can never produce a ghost reservation\n  a login could start on (injected-write-failure regression: no marker, no\n  requeue surprise, retry works)\n- Ownership is an opaque random attempt nonce (not createdAt): cancel and\n  the NEW successful-commit guard both validate it under the same store\n  lock. Add-account logins stage the OAuth credential under\n  mcp:<id>--<attempt> and move it to the real account key only inside a\n  guarded finalizeAttempt commit — a late login success after remove or\n  replacement discards the staged credential and never touches the\n  account (regressions: late-success-after-remove and same-id-new-owner,\n  both assert the final credentials)\n- Per-account status is one computed helper (accountStateFor): credential\n  binding, expiry, and the record combine through httpConnectionStatus, and\n  the plugin aggregate, connection inventory, and account picker all use\n  it — previously-connected records with unbound, retargeted, or\n  expired-no-refresh credentials report Reconnect everywhere (6\n  regressions across default and alias accounts)\n- Catalog metadata aliases ride the view DTO and are searchable again\n  (regression: an alias absent from label/id/description matches)\n- Vanished-source pins are their own trust path (never localSource trust;\n  never one-click connectable)\n\nTests: 147/147 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix(coding-agent): exactly-once account transactions and locked disconnects (ENG-6108)\n\nFinal-review wave 6 (transaction issues):\n\n- Every one-shot store callback (reserve/removeReservation/finalizeAttempt/\n  removeAccount) settles EXACTLY once: deferred callbacks resolve with the\n  commit outcome (not `didCommit && resolve(true)`), losing and throwing\n  branches resolve false inline, one-shot ops are dropped on write failure,\n  and trailing ops in a failed batch can never hang — batched-two-reserves\n  write-failure regression included\n- Account removal runs credential deletion AND record removal under the SAME\n  store lock (new store.removeAccount; disconnect, remove-action, and\n  /mcp logout all use it): a concurrent finalize can never re-add an orphan\n  credential after a disconnect — interleaving regression on real stores\n  covers both orders (remove-then-finalize loses ownership; finalize-then-\n  remove clears record AND credential)\n- finalizeAttempt compensates: if the RECORD write fails after the credential\n  move, the compensate callback runs under the same lock and restores the\n  staged credential (the real key was empty before the attempt), so a failed\n  finalize is all-or-nothing; the flow then releases the reservation and\n  reports honestly — UI regression asserts the real key untouched, staged\n  restored+discarded, reservation released, and the honest message\n\nTests: 151/151 across 10 files; npm run check EXIT=0; 10/10 consecutive\nscoped runs with zero unhandled rejections, exit 0 each.\n\n* fix: honest MCP account recovery edges (ENG-6108)\n\n- removeAccount logs a credential-only integration out even when no\n  connection record exists; outcomes are typed (removed, credential-only,\n  missing, failed) and every UI route maps them honestly.\n- finalizeAttempt registers its rollback BEFORE any side effect: a\n  throwing or partial commit is compensated under the same lock, and a\n  FAILED rollback surfaces an explicit recovery-required state with the\n  credential retained instead of a silent false.\n- the guarded commit refuses to clobber a credential an ordinary login\n  wrote at the account key mid-flight, and the rollback only ever undoes\n  its own write; a bystander credential survives byte-for-byte.\n- one-shot store operations settle cleanly when the file lock itself\n  cannot be acquired; durable operations requeue (pre-splice).\n\n* fix: atomic MCP credential moves and honest partial logouts (ENG-6108)\n\n- staged logins move their credential to the account key with a new\n  disk-authoritative compare-and-set under the auth backend's own file\n  lock: moveStagedCredential (set-if-absent, returns the exact moved\n  credential), restoreCredentialIfAbsent, and removeIfCredentialMatches\n  (exact full-object own check, not access-token equality). A bystander\n  credential written by another process can no longer be clobbered by a\n  get/set race across AuthStorage instances.\n- the finalize compensation restores ours only into an empty staged slot\n  and deletes only its own exact credential from the account key.\n- removeAccount now reports \"logged-out\" when the credential logout\n  committed but the record save failed: the logout is preserved, the UI\n  wording says so and never claims the account is still connected; plain\n  \"failed\" wording is state-neutral.\n- two-real-storage-instance regressions: a bystander login written after\n  staging survives byte-for-byte; a generic /logout in another client\n  before/after the finalize races atomically (last-writer-wins under the\n  backend lock, record shows an honest unbound state); an injected\n  record-write failure after the logout reports the honest partial state.\n\n* fix: disk-verified MCP logouts and logout-cancelled login attempts (ENG-6108)\n\n- the three removeAccount authCleanup paths now use removeVerified\n  (extended to return whether a credential was actually removed from\n  disk): a failed auth-file write can no longer report Removed/\n  Disconnected while the credential persists; the honest \"failed\"\n  wording is state-neutral.\n- a real generic /logout of an MCP account now cancels that account's\n  pending login attempts: runLogout calls the new\n  ProviderAuthFlowsHost.onMcpCredentialRemoved hook, and the host\n  invalidates the pending reservation under the connection store's lock\n  (new invalidatePendingAttempts), so a stale in-flight OAuth attempt\n  resolves denied and can never re-activate the logged-out account.\n  Non-MCP logouts are unchanged.\n- regressions: auth-file write failure with a fresh disk reader (the\n  credential survives, nothing claims success), a route-exact\n  cross-client pending-finalize vs real /logout test driven through the\n  selector, store-level invalidation semantics, and stale\n  interactive-mode-status fixtures repaired to the current seams.\n\n* fix: one shared store-locked MCP logout op for the generic /logout route (ENG-6108)\n\n- the route delegates the ENTIRE MCP logout before touching auth: the new\n  exported logoutMcpAccount(providerId, store, authStorage) performs\n  verified credential deletion (removeVerified) AND pending-attempt\n  cancellation in one store-locked removeAccount with the new\n  preserveCompletedRecord semantics (pending attempts cancelled,\n  completed records kept for the honest unbound/Reconnect state).\n- removeAccount now runs the verified cleanup BEFORE mutating the\n  working records map: a failed auth-file write resolves \"failed\" with\n  the record intact in memory AND on disk (no half-applied deletion),\n  and a partially-saved logout still reports the honest \"logged-out\"\n  state.\n- the id resolution is exported as resolveMcpAccountLogoutTarget:\n  exact ids win (service ids may contain \"--\"), and staged keys map\n  back only through their actually recorded attempt nonce.\n- regressions: a barrier test fires the REAL generic route inside the\n  finalize's locked commit (verified deterministically red against the\n  old logout-first ordering); store-level half-applied-deletion and\n  preserve-semantics tests; route delegation/wording tests; handler\n  tests for double-hyphen ids, staged-key mapping, and preservation.\n\n* fix: fail-closed refusal for stale staged-key MCP logouts (ENG-6108)\n\n- a staged-key logout that races a completing finalize now refuses\n  fail-closed instead of orphaning the moved credential: the shared\n  handler resolves the account from CURRENT records and CURRENT on-disk\n  auth state under the store lock (never a pre-lock cache snapshot), and\n  when the staged credential already moved onto the account key it\n  returns \"refused\" — the record and the newer credential both survive,\n  and the route reports the completed login explicitly instead of\n  claiming Logged out.\n- new removeAccountForProvider store op (one critical section: resolve\n  under lock -> disk-verified refusal checks -> verified cleanup ->\n  cancel pending / preserve completed); resolveMcpAccountLogoutTarget\n  now returns a stale variant for nonce-matched non-pending records.\n- new disk-authoritative AuthStorage.hasVerified existence check used\n  for the current-state refusal decisions.\n- regressions: the staged-key race queued behind a finalize-first move\n  (refused, record + credential survive), resolver stale semantics, and\n  the route-level refused wording.\n\n* fix: staged-key MCP logouts cancel the attempt and preserve the account shell (ENG-6108)\n\n- removeAccountForProvider no longer gates record deletion on an auth\n  existence snapshot (not atomic with another client's ordinary login):\n  a staged-key logout removes only the staged credential, INVALIDATES the\n  attempt nonce on the pending record (so the in-flight login's denied\n  path can neither finalize nor removeReservation-delete it), and\n  PRESERVES the account record — deletion stays the explicit Remove\n  action's job; a bystander credential on the real key can never be\n  orphaned.\n- the \"refused\" outcome is now state-neutral (\"This login attempt is no\n  longer current; manage the account from /plugins\") — no Logged out or\n  Connected claim from token presence; failed verified auth deletion\n  still changes no record.\n- the interim AuthStorage.hasVerified helper is removed (no auth-state\n  snapshot decisions remain); regressions pin the both-exist contract\n  (staged removed, bystander byte-for-byte, record + nonce-invalidated\n  shell preserved, old-nonce finalize denied, removeReservation no-op)\n  and the finalize-first race with the same invariants.\n\n* feat: one guarded connect operation for every MCP OAuth login route (ENG-6108)\n\n- new one-shot store ops claimConnectionId/releaseClaim: a guarded login\n  claims an EXISTING account record (any status) under the store lock and\n  cancel releases it — the record, its status, and any credential survive\n  untouched; finalizeAttempt ownership is now the EXPLICIT nonce on any\n  record status.\n- every user-facing MCP OAuth login (initial connect, error-state\n  reconnect, add account, /mcp login, generic /login service options,\n  config menu) routes through the guarded flow: claim/reserve, staged\n  OAuth, guarded finalize. The raw dialog remains internal.\n- the finalize commit is a full-identity compare-and-swap on the auth\n  backend lock: set-if-absent for fresh accounts, replace-only-if-matching\n  for intentional replacements (reconnects and credential-only legacy\n  grants); it consumes the nonce and marks the record pending-verification\n  so an old Connected status never carries onto a replaced grant; the\n  compensate RESTORES the previous credential (never delete-only, never\n  clobbering a newer writer).\n- every preserving logout clears the ACTIVE claim nonce regardless of\n  record status, so a logout cancels an in-flight reconnect too; a nonce\n  match in the resolver is always a LIVE attempt (finalizes consume\n  nonces, so the stale variant is gone).\n- new AuthStorage primitives: getVerified (disk-authoritative identity\n  capture) and replaceStagedCredential / replaceCredentialIfMatches\n  (full-identity CAS move and restore).\n- regressions: error-state reconnect (CAS, pending-verification, nonce\n  consumed, old verification state cleared), cancelled reconnect\n  preservation, late-reconnect-after-logout discard, claim/release one-shot\n  settlement on failed batch writes, rollback-restores-previous via CAS,\n  and the generic /login + config-menu delegation to the guarded hook.\n\n* fix: preserve MCP login intent and cancelled account settings (ENG-6108)\n\n* test: cover MCP account lifecycle commit safety (ENG-6108)\n\n* fix: merge-main regressions and secret-free grant binding (ENG-6108)\n\n* fix: fail closed on unproven MCP catalog entries and expired grants (ENG-6108)\n\n* fix: keep built-ins in the catalog cap and harden grant-type regressions (ENG-6108)\n\n* fix: match MCP connectors to the inline configuration pickers (ENG-6108)\n\n* fix: make MCP picker actions match settings-backed targets (ENG-6108)\n\n* feat(ai): complete the generic MCP OAuth engine (ENG-6108)\n\nThe MCP OAuth engine only handled public-client dynamic registration and\nexact endpoint/protected-resource matches: it discarded all registration\nresponse fields beyond client_id, requested every AS-advertised scope,\nissued unbounded uncancellable requests, and rejected the official\nNotion/Slack metadata shape (pathful endpoint, origin-level PRM resource)\nat discovery, so those servers could not log in or refresh at all.\n\nThe engine now follows the SDK 1.30 auth standards via pure helper/schema\nimports from @modelcontextprotocol/sdk (never the auth() orchestrator,\nwhose redirect model does not fit the staged consent UI):\n\n- Client auth-method negotiation (client_secret_basic > post > none,\n  DCR hint honored, RFC 8414 defaults) applied to exchange and refresh,\n  with a compatibility gate: a secret-only server never receives a doomed\n  no-auth request; it fails early with typed setup guidance.\n- Full RFC 7591 registration responses: persisted DCR client identity,\n  issued secret and expiry (respected on refresh), and registration-mode\n  binding. Config-supplied secrets are never persisted or embedded.\n- CIMD/SEP-991: a configured Prime-controlled client-metadata URL becomes\n  the client id when the server advertises\n  client_id_metadata_document_supported; no URL is invented or deployed.\n- SEP-835 scope precedence: configured > protected-resource > omitted;\n  authorization-server-wide scope lists are never blindly joined.\n- Narrow audience policy: the PRM resource must equal the canonical\n  endpoint or its exact HTTPS origin (URL-component compared); refresh\n  pins both values and the audience mode, fail-closed on any change, and\n  legacy exact grants never refresh against origin audiences.\n- Every request is bounded (10/15s deadlines, 256KiB bodies) and login\n  honors the host abort signal, including callback-server cleanup.\n- Token errors are typed and sanitized: whitelisted RFC error codes only,\n  fixed guidance, never raw server-controlled descriptions.\n\nAll flows run against offline fake OAuth/MCP servers in\npackages/ai/test/mcp-oauth.test.ts (39 tests: official Notion/Linear\nfixtures, Slack post-only negotiation, CIMD, refresh binding and\nrotation, abort/cancellation); no live endpoints or credentials.\n\n* feat(ai): read-only catalog metadata audit with readiness evidence (ENG-6108)\n\nAdd a reproducible read-only public metadata audit of all 103 remote catalog endpoints (public unauthenticated GETs only, bounded, DNS-pinned per request through an undici dispatcher lookup) and merge the committed evidence into the catalog offline. New additive, optional interface: setup.readiness (informational, never a Connect gate), setup.requirement, setup.fields[].kind, auth.alternatives[] per-path alternatives, and the observational auth.metadata evidence block. Placeholder-only OAuth blockers are cleared with evidence (Airtable, Shopify stay Connect-attemptable); genuine documented requirements stay hard (API keys, bearer tokens, tenant config, registered clients, transport and local-runtime limits). CIMD alone is never oauth-ready, audience-mismatched PRM documents stay unknown with notes until the approved engine audience policy lands, and local sources cannot self-assert readiness, alternatives or evidence.\n\n* docs(ai): correct stale oauth-marker consumer comment (ENG-6108)\n\nThe host keys OAuth support off authStrategy, not entry.oauth?.kind; the comment now points consumers at auth.strategy. Comment-only change.\n\n* fix(coding-agent): guard mcp account ownership and capability policy (ENG-6108)\n\n* fix(ai): adopt engine component comparison for readiness re-derivation (ENG-6108)\n\nThe importer audience rule now uses the engine component comparison: a protected-resource document must match the exact canonical endpoint or the exact origin (root-slash normalized). notion plus the 9 DCR-capable origin-mismatched entries (airtable, amplitude, appwrite, lovable, miro, rootly, vercel, windsor-ai, zoominfo) re-derive to oauth-ready and their stale not-yet-landed notes are gone; logrocket stays unknown (its resource drops the ?toolsets=all query, failing closed under the component rule); slack and hubspot stay unflipped (no dynamic client registration). Counts re-derive to 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown. Catalog oauth validation now rejects client secrets explicitly instead of silently dropping them, symmetric with the client-id rejection.\n\n* feat(coding-agent): wire oauth client identity through shared mcp provider factory (ENG-6108)\n\n* fix(ai): mirror engine PRM selection in the metadata audit (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hvVZP + PRRT_kwDOSXZbXs6hvVZS: auditEndpoint now mirrors oauth.ts exactly - a WWW-Authenticate resource_metadata pointer is followed alone and any failure fails closed with no well-known fall-through; absent a pointer the pathful well-known is tried then the origin-level root location (SDK parity, 4xx continues); a candidate is selectable only when its document passes the engine audience component comparison and carries authorization servers. protectedResourceUrl is used at the call site instead of an inline construction. The bounded read-only audit was re-run (public GETs only, DNS-pinned, all bounds unchanged) and readiness re-derived: the reviewer obligation flips stay intact; codspeed and resend flip to oauth-ready via the SDK-parity origin-level fallback (valid documents, DCR, coherent AS); confidence-docs/-flags stay unknown with fail-closed notes (served-but-invalid documents); logrocket stays unknown with the precise fail-closed reason; slack/hubspot stay unflipped (DCR-less); shopify metadata is now available but DCR-less. Counts: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown; metadata 97 available / 6 unavailable.\n\n* fix(ai): require the exact engine fallback condition for oauth-ready (ENG-6108)\n\nBugbot PRRT_kwDOSXZbXs6hva2k: evidenceSupportsStandardOauth granted oauth-ready via origin-AS evidence for ANY no-served-JSON state, but the engine reaches its origin-level AS fallback ONLY when no resource_metadata pointer was followed and every tried well-known candidate returned exactly a 4xx (5xx, fetch errors, 2xx/3xx non-JSON bodies and served-but-invalid documents all throw with no fallback). A precise engineFallsBackToOriginAs predicate is now derived from the recorded attempts (followed or rejected pointer, served document, and per-attempt httpStatus), so fail-closed discovery stays honestly unknown. The audit script selectionNote now fires only for true fail-closed states; the all-4xx state is the engine honest fallback and records no note. The bounded read-only audit was re-run to match the corrected note semantics (aggregates stable: 103 audited, AS 97/6, DCR 78, CIMD 25, PKCE 97). Zero readiness flips - pure hardening; 7 all-4xx entries lose stale generic notes; new regression assertions pin intercom (oauth-ready via the legitimate origin-AS fallback, no note) and adobe-for-creativity (unknown, unavailable AS, no note). Counts unchanged: 72 oauth-ready / 53 user-setup / 5 prime-restricted / 10 unknown.\n\n* fix(ai): classify figma as registered-client after live DCR 403 evidence (ENG-6108)\n\nDogfooding PR #2256 head 8e77f0cc1 surfaced what the read-only audit could\nnot observe: Figma's advertised dynamic-client-registration endpoint returned\nHTTP 403 to the engine's anonymous registration POST during a real\n/mcp login figma attempt (2026-09-13), while its AS metadata still advertises\nthe endpoint with confidential-only client auth. A public metadata-only GET\nof the register endpoint returns 404, and public reports corroborate the 403\nfor anonymous DCR requests; whether self-registered Figma apps can obtain\nmcp:connect is publicly unresolved.\n\n- Audit store: the live attempt is recorded as an authorizationServer\n  registrationAttempt block with explicit provenance (the audit script never\n  POSTs registration endpoints); the script now preserves these blocks\n  verbatim on re-runs, and the $comment sanctions exactly this supplement.\n- Importer: an advertised registration endpoint whose live anonymous\n  registration was rejected (401/403) is gated evidence —\n  auth.metadata.dynamicClientRegistration flips to an explicit false (gated,\n  distinguishable from omitted \"not advertised\"), the gated finding becomes\n  the default metadata note, and the entry can never be oauth-ready\n  (fail-closed, the same honest class as DCR-less Slack/HubSpot).\n- overrides.json: figma mirrors the Slack precedent — pre-registered client,\n  registered-client requirement, prime-restricted readiness, requires-setup\n  with the 403-documented reason and FIGMA_MCP_CLIENT_ID / FIGMA_MCP_CLIENT_SECRET\n  setup fields.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: ready 82 -> 81, requires-setup 58 -> 59, oauth-ready 72 -> 71,\n  prime-restricted 5 -> 6 (user-setup 53, unknown 10 unchanged; sum 140).\n\nOffline verification: catalog suite 25/25, coding-agent catalog eligibility\n12/12, tsgo --noEmit clean, and both catalog scripts typechecked under the\nrepo base config.\n\n* drop the revert changelog fragments\n\nthe reland makes the \"removed\" entries moot before any release cut them;\nthe release notes should only describe the additions.\n\n* feat(ai): cut the service catalog to zero-app self-serve only (ENG-6108)\n\n2026-09-14 product decision: Prime maintains ZERO provider OAuth apps, so\nthe catalog advertises only what works self-serve — dynamic client\nregistration (readiness \"oauth-ready\") or user-supplied tokens/keys\n(\"user-setup\"). Every entry classified prime-restricted (needs a\nprovider-registered client) or unknown (no verified self-serve path) is cut\nfrom the shipped catalog; audit/engine parity, the readiness vocabulary and\nthe fail-closed classification machinery are unchanged.\n\n- overrides.json: the 16 cut providers (figma, gmail, google-calendar,\n  google-drive, mongodb-atlas, slack, adobe-for-creativity,\n  confidence-docs, confidence-flags, hubspot, logrocket, mapbox-docs,\n  shopify, sumup, synthflow, synthflow-docs) are excluded via the documented\n  excludedServers list — one entry per upstream record (18 keys: figma and\n  slack are merged from both upstreams, so both sides are cut and no ghost\n  entry can re-enter), each with a per-entry reason citing the decision and\n  the specific audit evidence. The now-dead servers overrides for excluded\n  identities were removed; their honesty content lives on in the exclusion\n  reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  an entry whose readiness classifies prime-restricted or unknown and fails\n  with a curation prompt (exclude with a documented reason, or curate\n  readiness with evidence), so an unverified provider can never ship\n  silently again. The gated-DCR honesty machinery (advertisedRegistrationGated,\n  explicit-false dynamicClientRegistration semantics) is kept and now\n  exported for direct unit tests, since the live case (figma) is excluded\n  rather than classified; the predicate still guards future entries.\n- Audit store: untouched — the pinned source snapshots and metadata-audit\n  results for the excluded endpoints stay committed as evidence (including\n  figma's live 403 registrationAttempt). The audit script now also retires,\n  never drops, registration-attempt evidence for servers that leave the\n  audit target set via the new top-level retiredRegistrationAttempts array,\n  so a re-run can never silently delete the figma evidence.\n- Regenerated catalog.json + catalog.data.generated.ts via the importer;\n  counts: total 140 -> 124 (http 102 -> 86; stdio 32, templated 5, sse 1\n  unchanged), ready 81 -> 71, requires-setup 59 -> 53, oauth-ready 71,\n  user-setup 53, prime-restricted 6 -> 0, unknown 10 -> 0 (sum exact).\n- Tests: mcp-catalog.test.ts now asserts the cut (all 16 ids absent, both\n  upstream sides of merged providers excluded, all 18 keys documented in the\n  import report with zero-app reasons, source snapshots and audit evidence\n  kept incl. figma's 403 attempt), pins the gated-DCR machinery directly\n  with synthetic evidence, updates the count assertions (124/71/53/0/0) and\n  keeps the byte-for-byte regeneration pin; the figma assertions now assert\n  exclusion instead of prime-restricted-in-catalog.\n\nOffline verification: catalog suite 27/27, coding-agent catalog eligibility\n12/12 (plus all 13 coding-agent mcp test files: 281 passed, 1 skipped),\ntsgo --noEmit clean at the repo root, both catalog scripts typechecked\nclean under the repo base config, biome clean on the changed files.\n\n* test(coding-agent): adapt activation-queue route host to the inline auth panel host surface\n\n* test(coding-agent): drive logout route assertions through the inline auth panel surface\n\n* fix(ai): demote confidential-only AS providers to the user-setup OAuth path (ENG-6108)\n\nLive gap found in dogfooding: Hugging Face /mcp login failed at connect with\n\"no compatible client authentication method (advertised: client_secret_basic,\nclient_secret_post)\" because the catalog classified the entry oauth-ready\nwithout ever checking token_endpoint_auth_methods_supported; the engine checks\nit at runtime and fails closed.\n\n- Extract the engine's fail-closed client-auth decision from\n  negotiateAuthMethod into the exported decideClientAuthMethod and add the\n  catalog predicates tokenAuthMethodsSupportPublicClient /\n  tokenAuthMethodsSupportConfiguredClient in packages/ai/src/mcp/oauth.ts —\n  the importer runs the SAME decision, so classification and the connect-time\n  gate share one source of truth.\n- Wire the gate into importer readiness: coherent DCR evidence with\n  confidential-only advertised methods now demotes honestly to the user-setup\n  OAuth path (requires your own OAuth app reason, registered-client\n  requirement, client-id/client-secret setup fields) instead of oauth-ready —\n  the user's own registered app, zero-app compliant, never an exclusion.\n  Advertised methods that serve not even a configured secret-bearing client\n  (mTLS-only) keep failing closed as today; omitted lists stay oauth-ready\n  per the engine's public-client spec default.\n- Regenerate catalog.json, catalog.data.generated.ts and import-report.json:\n  14 entries demote (airwallex, airwallex-sandbox, atlan, gitlab,\n  hugging-face, legalzoom, lusha, miro, monday-com, planetscale, supabase,\n  vercel, windsor-ai, zoominfo) — readiness counts move 71->57 oauth-ready and\n  53->67 user-setup; total 124 and zero prime-restricted/unknown invariants\n  hold, each entry carrying its confidential-only evidence and note.\n- Document tokenAuthMethods as required engine-compatibility capture in the\n  audit script and catalog README so future audits keep the check.\n- Tests: pin the hugging-face demotion with its confidential-only evidence and\n  the full demoted set, add the synthetic classification matrix\n  (confidential-only demotes, none/omitted stay one-click, mTLS-only stays\n  unknown) and direct predicate tests; update readiness count pins. The\n  byte-for-byte regeneration pin and the coding-agent eligibility thresholds\n  pass unchanged.\n\n* test(coding-agent): pin the MCP auth routes to the inline auth panel surface\n\nthe guarded-credential race test kept an unobserved showOverlay stub on the\nfake route TUI; it now asserts the generic /logout service selector never\nmounts an overlay popup, so MCP auth can only render through showAuthPanel\nlike #2331 provider logins. adds the changelog fragment documenting that\nevery MCP auth surface (guarded staged OAuth dialog, /login and /logout\nservice selectors, mcp-connections tab, /mcp login) mounts inline under\nthe chat with stacked closers, focus restore, and rows-aware sizing.\n\n* feat(ai): cut the catalog to one-click DCR or user token/key only (ENG-6108)\n\n2026-09-15 product decision (\"remove everything that's not one-click auth\nor api key\"): the shipped catalog advertises only one-click dynamic client\nregistration (readiness \"oauth-ready\") and user token/key entries; the\nclassification machinery and audit/engine parity are unchanged.\n\n- overrides.json: the 49 non-token/key entries — the 14 registered-client\n  entries whose authorization servers accept only secret-bearing client auth\n  (airwallex, airwallex-sandbox, atlan, gitlab, huggingface-skills,\n  legalzoom, lusha, miro, monday-com, planetscale, supabase, vercel,\n  windsor-ai, zoominfo), the 32 local-runtime stdio adapters and the 3\n  url-only tenant templates (activecampaign, jfrog, pigment) — are excluded\n  via the documented excludedServers list: one entry per upstream record\n  (64 keys: monday-com/supabase/vercel are merged from both upstreams, so\n  both sides are cut; every stdio server of the multi-server adapter plugins\n  is excluded), each with a per-entry reason citing the decision and the\n  specific evidence. The now-dead servers overrides for excluded identities\n  were removed; their honesty content lives on in the exclusion reasons.\n- Importer: the cut is enforced structurally — buildCatalog refuses to emit\n  a user-setup entry whose requirement is not token/key-shaped\n  (bearer-token/api-key credentials, a tenant config with a genuine non-url\n  field, or a legacy-transport entry that still collects a token/key) and\n  fails with a curation prompt, so a registered-client demotion, a\n  local-runtime adapter or a url-only template can never silently re-derive.\n  The auth-methods honesty machinery (decideClientAuthMethod parity,\n  confidential-only demotion, tokenAuthMethods capture) is unchanged and\n  stays active for the survivors.\n- Audit store and source snapshots: untouched — evidence for the excluded\n  endpoints stays committed as history.\n- Regenerated catalog.json + catalog.data.generated.ts + import-report.json\n  via the importer; counts: total 124 -> 75 (http 86 -> 72, templated 5 -> 2,\n  sse 1 unchanged, stdio 32 -> 0), ready 57 unchanged, requires-setup 67 ->\n  18, oauth-ready 57, user-setup 18, prime-restricted 0, unknown 0 (sum\n  exact); api-key strategy 32 -> 16.\n- Tests: mcp-catalog.test.ts pins the cut (all 49 ids absent, both upstream\n  sides of merged providers excluded, all 64 keys documented in the import\n  report with 2026-09-15 reasons, source snapshots and audit evidence kept,\n  the shipped user-setup set is exactly the 18 token/key entries), keeps the\n  confidential-only machinery matrix (now pinned on the committed\n  huggingface-skills audit evidence plus the synthetic matrix), adds the\n  structural invariant regression (lifting the gitlab exclusion fails the\n  import with the curation prompt), updates the count assertions (75/57/18/\n  0/0) and keeps the byte-for-byte regeneration pin; coding-agent\n  mcp-service-catalog count comment + eligibility sweep thresholds updated\n  for the 75-entry catalog (api_key >= 10 still holds at 16; unknown 53).\n\nOffline verification: ai catalog suite 29/29, ai mcp-oauth 40/40, all 13\ncoding-agent mcp test files 276 passed 1 skipped (catalog eligibility 12/12,\nservice catalog 63/63), tsgo --noEmit clean at the repo root, biome clean on\nthe changed files.\n\n* fix(tui): separate the oauth url from the browser-step text with a blank line\n\n* fix(tui): keep the service-catalog picker at one description line with stable frames\n\n* feat(tui): persist MCP connect outcomes as refinement-style chat entries (ENG-6108)\n\nConnect outcomes (\"Connected Linear (12 tools verified)\" and its\nsaved-but-unverified variants) were transient status lines that vanished;\nthey now persist in the chat transcript like harness refinement notices.\n\n- Add the mcp_connection_outcome custom message with structured details\n  (label, login/retry source, connected/unverified/unsaved verification\n  state, tool count, account id, activation) and the exact legacy wording\n  as its content; keep it out of LLM context like refinement outcomes.\n- Render it with a purple refinementHeader \"◆ Connected <service> · N\n  tools verified\" header over a refinementSummary body via the\n  ExpandableEventMessage base; register live rendering and transcript\n  replay (malformed entries included) and gate both on display.\n- Route both emit sites (pending-account retry verification and the\n  guarded login completion) and their failure/partial variants through\n  completeMcpConnectionOutcome: reload, then append the durable entry via\n  the new AgentConnection.appendCustomMessage (in-process sendCustomMessage,\n  daemon legacy append_custom_message RPC). Mid-turn connects keep the\n  transient \"will activate automatically\" line and record the durable\n  entry at the next safe boundary; a failed reload marks the entry\n  \"not active in this session\". Pre-outcome login failures (eligibility,\n  claim, finalize, incomplete login) remain transient warnings.\n- Focused tests: component rendering/replay, formatter wording parity,\n  and both emit sites including queued-boundary, inactive, and\n  append-failure fallback paths.\n\n* fix(tui): flatten multi-line catalog copy before it becomes a picker line\n\n* fix(tui): repair /mcp picker chrome nits and search ranking (ENG-6108)\n\nFive chrome nits from Kevin's live testing plus the picker search:\n\n- The scroll counter aligns with the row indent (rows render their label\n  at column 2; the counter and the empty-state message now do too).\n- Enter on an accounts-picker row 1 (the account name) re-verifies the\n  account instead of disconnecting it; only the explicit Remove row\n  removes. connectServiceFromPicker's connected branch now re-verifies\n  exactly like the pending branch.\n- One blank line between the last row and the description line in both\n  catalog and accounts mode, budgeted in updateLayout so frames stay\n  height-stable.\n- Accounts search matches identity only (account label + connection\n  id): every account row shares the same description/setup hint, so\n  matching that text could never filter anything.\n- Every inline MenuPanel opens with exactly one separator rule between\n  the chat view and the picker: a titled panel draws the rule above its\n  title; a headerless panel keeps the bordered search's top border as\n  that rule, never both. Model/provider/oauth/extension/prime-team\n  pickers budget the rule through the shared inlineMenuPanelTopRuleRows\n  helper.\n- Search ranking rewritten: exact/prefix/word-start/substring matches on\n  label, service id, and aliases rank far above description/setup-hint\n  text; the identity-only subsequence fallback requires a consecutive\n  run of half the query (minimum two); descriptions match only as\n  word-start/substring. Searching vercel against a catalog without\n  Vercel now yields the No-matching-services empty state instead of\n  eight unrelated rows matched through setup-hint boilerplate.\n\n* fix(mcp): cut non-token catalog survivors and say what to paste\n\n2026-09-16 product decision, from live picker testing: the catalog ships\none-click DCR connectors and simple paste-an-API-key/token services only.\n\nCut 5 survivors with documented per-entry reasons in overrides.json:\n- cockroachdb (cockroach-labs): tenant config, only input is the\n  per-cluster `mcp-cluster-id` header, no key/token field\n- dynatrace, sourcegraph: tenant instance URL next to the token\n- paypal-sandbox: legacy SSE, the runtime cannot connect to it at all\n- render: api-key requirement with zero setup fields, nothing to paste\n\nThe importer now enforces the narrower rule structurally: a user-setup\nentry ships only when its requirement is bearer-token or api-key AND it\ncollects at least one bearer-token/api-key field. An entry the readiness\npass never classified also fails closed instead of slipping past every\nshipping guard.\n\nAll 13 surviving setup reasons are now picker copy for a human: one line,\nstarting with \"paste\", naming the entry's real field. \"requires provider\ncredentials supplied as headers\" and \"requires an auth token supplied via\nenvironment variable\" are gone; github no longer stutters the same clause\ntwice; zoom-meetings drops the upstream placeholder note that never\nchanged what the user does. A test pins the shape.\n\nCatalog: 75 -> 70 entries (57 oauth-ready, 13 user-setup), all plain\nstreamable-http. Evidence, source snapshots and audit store unchanged.\n\nENG-6108\n\n* fix(tui): drop the restated MCP connect body and add disconnect feedback\n\nKevin saw the durable connect entry print the same sentence twice: the\nheader and the body both said \"Connected Granola (6 tools verified)\".\nThe header is the outcome line, so the body now carries ONLY what the\nheader omits - added account, verification issue plus next step, deferred\nactivation - and a plain success renders as the header alone. The\npersisted `content` keeps the full legacy sentence, because the transient\nfallback and status lines still need a whole sentence.\n\nDisconnects had no durable feedback at all. A successful disconnect from\n/plugins, `/mcp logout <name>`, or the generic `/logout` route now leaves\nthe same kind of entry, in the muted theme key: not the connect purple (a\ndisconnect is no refinement) and not the error red (it succeeded) - the\ngrey /plugins already uses for \"Not connected\". Only outcomes that really\nremoved the credential (removed, credential-only, preserved) get an\nentry; \"logged-out\" (partial save failure), \"failed\", \"missing\", and the\nstate-neutral \"refused\" keep their honest warnings and record nothing, so\nno entry can claim a disconnect that did not finish.\n\nThe generic /logout route still runs ONE store op and ONE reload: the\nhost records the outcome under its existing delegation and the selector\nappends it after the reload it already did.\n\nENG-6108\n\n* fix(tui): record the picker disconnect entry on the remove row\n\n* fix(tui): rebuild the /mcp accounts menu as a choice list (ENG-6108)\n\nKevin's live-testing redesign: the accounts menu drops its no-op search box\nand account-name row, renders '<service> MCP' with the muted description\n(wrapped, capped at three lines) above the options, and lists Reconnect /\nDisconnect <id> / Add another account in the PR #2340 selection language\n(mirrored locally until #2340 lands). Reconnect keeps the existing re-verify\npath and never disconnects; Disconnect keeps the store-locked removal and\nits durable disconnected entry.\n\nCatalog picker nits: the Connect trailing status reads in the text colour\ninstead of the accent purple, and the empty state gains one blank row above\nthe shortcuts line, aligned with the row labels (pinned at width 120).\n\n* fix(tui): stop inline pickers doubling the separator rule\n\n* feat(mcp): connect paste-a-key catalog services through an inline masked paste panel (ENG-6108)\n\n* fix(mcp): collapse the paste flow to ONE credential per service, cut named-header pairs (ENG-6108)\n\nTwo Bugbot findings, one root cause: the generic MCP runtime sends exactly\none Authorization: Bearer per connection, but the paste flow treated every\nrequired credential field as a separate mandatory prompt, and GitHub's two\nsetup fields (GITHUB_PAT_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN) were presented\nas two required tokens when they are alternative names for one PAT.\n\n- Curate GitHub's fields as alternatives via a shared field-level\n  credentialSet (\"github-pat\") in mcp-catalog/overrides.json; the paste flow\n  now prompts ONCE, labels honestly (\"GitHub personal access token\") and\n  stores the value under the first alternative's id.\n- Cut datadog and cloudinary-mediaflows from the catalog (documented\n  excludedServers reasons citing the named-header evidence: DD_API_KEY +\n  DD_APPLICATION_KEY, cld-api-key + cld-secret - one bearer can never\n  authenticate them). Catalog: 68 entries = 57 one-click DCR + 11 paste.\n- The importer now throws when a shipped requires-setup entry collects more\n  than one distinct required credential, so an unmarked multi-credential\n  entry can never silently re-derive; tests pin the guard by lifting the\n  datadog and cloudinary exclusions.\n- Simplify the host flow to one prompt/one value (no multi-field loop, no\n  per-field checkmark rows); masking, Esc-cancels-nothing-stored, the\n  empty-value guard, verification and the durable Connected outcome are\n  kept; services with genuinely distinct credentials stay honest\n  setup_required and are not pasteable.\n\n* fix(mcp): drop the dead paste-credential branch and trim the importer comment\n\n* fix(tui): render paste outcomes honestly and polish the accounts menu\n\n* fix(tui): budget the accounts frame for the trailing blank row\n\n* fix(tui): keep the /mcp picker chain open after connect, disconnect, and paste actions\n\nSelecting a row no longer drops the user back to the prompt after the action: the picker chain re-enters the right surface, rebuilt from the live catalog and connection store. Disconnecting one account reopens that service accounts menu without the removed row; disconnecting the last account, or a cancel that stored nothing, reopens the catalog; reconnect/re-verify, add-account, and paste re-enter the accounts menu with refreshed state. Esc closes the whole chain, and actions blocked before they start (login in progress, guidance, target drift) end the chain with their status line instead of re-opening, so there is no re-entry loop.\n\n* fix(tui): drop the accounts description instead of overflowing tight viewports\n\n* fix(tui): one reconnect/disconnect row per service with an account sub-picker, and left-arrow back to the catalog\n\nA service's accounts menu is now exactly three rows — Reconnect, Disconnect, Add another account — instead of a reconnect/disconnect pair per connection id. With one account, Enter acts on it directly with the existing re-verify and remove semantics. With several accounts, Enter on Reconnect or Disconnect opens a second inline picker in the same choice-list shape that lists the account ids; picking one runs the action on that connection id only, and Esc or left there returns to the accounts menu without acting. The sub-picker never nests deeper, and after an action the existing re-entry table decides the next surface (the accounts menu while an account survives, the catalog otherwise).\n\nLeft arrow from the accounts page now navigates back to the /mcp catalog — the picker chain's parent surface — remounted fresh, using the same app.modal.back binding the dialogs share; Esc still closes the whole chain, and left in the catalog stays inert. The accounts frame budget is unchanged (the option count is the only thing that grows), the budget test now pins the three-row count, and the multi-account re-entry tests drive the sub-picker end to end.\n\n* fix(tui): colour mcp outcome entries by outcome, warning for non-success\n\n* fix(mcp): keep alias OAuth providers behind the parent service catalog classification (ENG-6108)\n\n* fix(mcp): probe the origin-level oauth-protected-resource document without the endpoint query\n\n* test: bring ENG-6108 suites into the main-branch testing policy (#2336)\n\nShrink the branch's test additions from a net 16,514 to 16,610? -> net 11,610\ntest lines (the same as the branch's source additions), clear all 46 new\ntest-policy pattern violations, and consolidate per the policy's \"one file\nper module, regressions live in the owning suite\" rules. No test survives\non a timer, poll, sleep, skip, or env gate; every wait resolves on a\nconcrete deferred signal (mount hooks, probe gates, store flushes).\n\nFolded per-issue regression files into the owning module suites:\n- suite/regressions/eng-6108-inline-picker.test.ts -> the picker-chain\n  describe in test/service-catalog-picker.test.ts (mount/stale/replacement\n  settling, action routing, re-entry, settings-actions freeze, paste flow,\n  blocked actions) with vi.waitFor replaced by ui.setFocus-mount and\n  action-start deferred signals; the pure row-shape/menu layout pins\n  already covered by the component tests were deleted.\n- suite/regressions/eng-6108-service-catalog-activation.test.ts -> the\n  connect-then-activate end-to-end test in test/agent-session-services\n  .test.ts; the stored-token-pending duplicate died (already pinned in\n  mcp-service-catalog).\n\nDissolved test/mcp-account-commit-safety.test.ts; its unique invariants\nmoved to the owning suites and the rest were duplicates:\n- store->auth gap interleave + finalize nonce ownership + no-resurrection\n  -> test/mcp-connection-store.test.ts\n- CAS-absence replaceStagedCredential -> test/auth-storage.test.ts\n- /mcp login on a PENDING account, vanished-source repair (record+grant\n  evidence; bare shell refuses), and the paste-flow seam (single-credential\n  static token, cancelled/empty value, login-in-progress and target-changed\n  refusals) -> test/mcp-activation-queue.test.ts.\n\nConsolidated near-identical seams (surviving test -> invariant it owns):\n- grant-state matrix pinned ONCE in mcp-catalog-eligibility (predicate +\n  prompt + dispatch + view parity); manager's copy deleted.\n- reserved-name ownership pinned at dispatch level in mcp-manager; the\n  view-level copy and mcp-service-safety's provider-lifecycle duplicate\n  deleted.\n- cap protection pinned in mcp-service-catalog (resolver's own module);\n  mcp-catalog-eligibility's copy deleted.\n- remove/logout-vs-finalize races pinned once at the REAL route seam in\n  mcp-activation-queue; the store-level interleave duplicate deleted.\n- exact rendered copy kept only where it is a durable protocol contract\n  (outcome notice wording); component render pins made structural.\n- oauth client-auth negotiation + PRM-mismatch refusals + python discover\n  cursor/table families -> it.each/subTest tables.\n\nViolation conversions (all 46 cleared):\n- wall-clock-timer/poll/sleep: setFocus-mount signals, action-start\n  deferreds, queueVerifyResult+flush chains, an asyncio.Event hang, and\n  fs.watch for the multi-process ready barrier.\n- environment-gated-path: the 11 false positives in mcp-oauth (mock route\n  conditions carrying token/credential substrings) renamed via aliases;\n  the rest made unconditional.\n- explicit-test-timeout: allowed with policy comments only where the\n  probe's own timeout bound IS the behavior under test (probe hang tests,\n  real multi-process spawn).\n- conditional-or-disabled-test: kernel-bootstrap made unconditional\n  (file-scoped venv resolution, no cwd dependence; merge of main's #2336\n  had dropped the branch's theme bootstrap in menu-panel too - restored).\n\nVerified: every modified suite passes standalone and under 6 shuffle\nseeds; npm run check (biome + check:test-policy + tsgo + installer +\nbrowser-smoke) is green; spot-reverts of the chain re-entry rule and the\nstore finalize-ownership guard both fail the consolidated tests.\n\n* fix(mcp): mirror the engine audience rule and first-issuer selection in the audit\n\n* fix(mcp): fail closed on a malformed advertised issuer and fix the notion URL substring check"},{"name":"multi-account-credential-pool.md","kind":"file","size":819,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"nix-skill-guards-and-fmt.md","kind":"file","size":233,"lastCommitHash":"5141d70b5314bce8f567348a6e9b93e6f2287204","lastCommitWhen":"2026-09-17T06:20:35+03:00","lastCommitMessage":"fix(skills,runtime): exempt skill callers from bash guards, add nix.fmt, and support awaitable results\n"},{"name":"pasted-files.md","kind":"file","size":282,"lastCommitHash":"e5f052c63af15817e86a4862789d0e08982d4878","lastCommitWhen":"2026-09-17T12:13:45+03:00","lastCommitMessage":"refactor(coding-agent,tui): call the feature a pasted file, one line and no hash\n"},{"name":"perf-session-switching-and-startup.md","kind":"file","size":340,"lastCommitHash":"a584201e8f472806873357e95741905e3d86e7d3","lastCommitWhen":"2026-09-17T06:20:35+03:00","lastCommitMessage":"perf(coding-agent): cache resolved tool paths and reuse UI services across session hops\n"},{"name":"pin-agent-sessions.md","kind":"file","size":143,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"prompt-cache-timer-and-usage.md","kind":"file","size":86,"lastCommitHash":"d9058cafe0916d6ad2c3e29abc07c72db037d1cb","lastCommitWhen":"2026-09-17T06:14:47+03:00","lastCommitMessage":"feat(coding-agent): add prompt cache timer and usage indicator to tray and agents view\n"},{"name":"provider-lane-health.md","kind":"file","size":299,"lastCommitHash":"d0913ca17f0784abfa2ae053d365fe7f9e9a4051","lastCommitWhen":"2026-09-17T10:01:48+03:00","lastCommitMessage":"fix(coding-agent): honor provider quota deadlines and preflight lane health\n"},{"name":"provider-quota-breaker-and-spawn-preflight.md","kind":"file","size":484,"lastCommitHash":"79ad21b7e0b08ad3c0c3bdee2691e259ce86bfa7","lastCommitWhen":"2026-09-17T07:01:56+03:00","lastCommitMessage":"fix(ai,coding-agent): stop retrying a depleted account and refuse the spawn (R2, R1)\n\n429 quota exhaustion was classified as a transient hiccup, so swarms looped 70+\ntimes into a hard wall (01a08f07, 01a0a1ab produced 405 turn stop errors\nagainst exhausted accounts) and 8 children were stillborn at turn 0.\n\n- a 429 whose provider text names account depletion (quota, credit, billing)\n  is now the permanent kind `quota_exhausted`; a keyword-free 429 stays\n  `rate_limit` and keeps its backoff. The keyword test runs only inside the\n  rate-limit branch, so a 400 that merely says \"quota\" stays invalid_request\n- `isPermanentProviderFailureKind` treats it as permanent, so the retry loop\n  stops instead of hammering depletion\n- a draining response trips a rolling in-memory provider breaker\n  (`blockedUntilMs` from the provider's retry hint, else 60s), and\n  `ModelRegistry.assertRlmSpawnAdmission` refuses a child at turn 0 with a\n  typed `RlmSpawnAdmissionError` (reason `quota_exhausted` or\n  `auth_unconfigured`) before any child directory, session, or provider request\n- `rlm.models()` reports tripped providers under the reserved `exhausted` key,\n  keeping its `Record<string, string[]>` contract; the runtime shim degrades to\n  [] when the key is absent\n- in-stream provider error paths (anthropic SSE, responses error/response.failed)\n  now pass the provider's own text to the classifier; they previously passed\n  only the error type, which hid the audited Alibaba-style exhaustion message\n\nLocal state only: no new daemon command, event, or payload shape, and no\nnetwork request in the preflight. Tests: packages/coding-agent/test/suite/\nprovider-quota-circuit-breaker.test.ts (10) and subagent-admission-preflight.test.ts\n(6, including a turn-0 refusal through runRlmChild); packages/ai stream-failure\nand openai-codex-stream (78).\n"},{"name":"quiet-bash-handle-completions.md","kind":"file","size":221,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"r12-isolated-subagent-workspaces.md","kind":"file","size":391,"lastCommitHash":"e88e3a28a2336ad92bfe06c3725c290fb0e9d54e","lastCommitWhen":"2026-09-17T07:04:26+03:00","lastCommitMessage":"feat(runtime,skills): one calling convention, quiet attenuation, disambiguated edit, network perimeter\n\nR7 S1 (the calling convention) - the cell boundary awaited only coroutines, so a\nskill that returned a plain value made `await skill()` raise \"object X can't be\nused in 'await' expression\" (156 of those in the audit corpus). The evaluator now\nsettles whatever a call returns, `f(x)` and `await f(x)` both work, and every\neager result is settled once; `BashHandle` and a started `asyncio.Task` stay plain\nvalues on purpose, because awaiting them would silently turn `bash(cmd)` into a\nblocking one-shot.\nR7 S4/S5/S6 (runtime half) - one generated surface per skill: a line per published\ncall plus a generated shape line per result type, a byte-capped signature digest,\nand an error boundary that appends the resolved signature, the nearest legal name,\nand the correction for a wrong keyword, a missing positional, awaiting a settled\nvalue, and a wrong attribute. A call that fails the same way twice gets one\nin-band repeat notice (second occurrence, then every fifth) and is never refused.\nThe `signatures` kernel request is additive: an older host never sends it and the\nruntime falls back to indexing the wrapped modules in the cell namespace.\n\nR6 (capability attenuation) - a subagent pushed unreviewed work to origin and ran\ndestructive repository commands (Sev 5, 01a08d13, 01a09632). `git`/`jj` skills now\nrefuse a publish or a force reset/restore from a child unless the parent authorized\nit, and the refusal names the correction (`allow_push=True` at spawn, or\n`RLM_ALLOW_PUSH=1`). Depth-0 sessions are unaffected.\n\nR7 S3/S7 (skills half) - `git`/`jj` take `cwd` for the repository directory and\n`paths=` for target files (`n` -> `limit` on the log/graph calls), the fact/action\nsplit is consistent (collections iterable, shapes generated, no hand-written\nsignature text), and `RemoteHandle.output` is a property.\n\nI1 - a non-unique `old_str` in the `edit` skill now changes nothing and reports the\nbounded line ranges of every occurrence with numbered candidate snippets, and\n`line_hint=<a line inside the wanted range>` resolves it in one retry. Exact-match\nsafety is unchanged.\n\nI6 - a command that would bind a server to a public interface is refused with the\nloopback form to use instead (`RLM_ALLOW_PUBLIC_LISTEN=1` for a deliberate public\nbind), and a hardcoded dev port warns with the ephemeral-port correction. Read-only\npipelines and ssh stay permissive; the R5/R8 guards are untouched.\n\nI5 is closed as superseded by R7 S6.\n\nTests: prime-agent-runtime suite 425; git 15, jj 23, search 11, edit 13 python\ntests; new test_skill_surface.py (20) and test_subagent_workspace.py (16).\n"},{"name":"r14-ipython-cell-deadline.md","kind":"file","size":879,"lastCommitHash":"564d037134a5c79634beaeeea23243f644b87a36","lastCommitWhen":"2026-09-17T07:02:39+03:00","lastCommitMessage":"feat(coding-agent): give an ipython cell a deadline (R14)\n\nA cell had no clock anywhere on the path: `repl-manager.ts:1079`\n`return await result.promise` parked the tool and the agent loop, so the only\nbound was a human abort. The audit's tail is that defect: 164 aborted turns, a\n92-minute deadlock, a 61-minute blocking await, and 21-92 minute $HOME walks.\n\n- `settings.json { \"cellDeadlineMs\": N | \"off\" }` (default 60 000, from the\n  measured corpus: 1 193 cells ran past 60s in four days and 907 of them were\n  bash-handle polling loops, while only 24 were real builds)\n- the manager arms a timer through the existing abort -> interrupt -> busy-wait\n  -> kill path, so `ExecuteResult.status` stays \"aborted\" and only the host-side\n  `timedOut` / `details.timeoutMs` / `details.kernelRestarted` are added\n- the `ipython` schema gains `timeout` (seconds, 1..3600); a larger value is\n  clamped and the clamp is reported in the cell's content\n- the `<ipython_cell_timeout>` notice says what happened, that earlier state and\n  this cell's partial output survive, and how to avoid it: detach long work, or\n  re-run one genuinely long call with `timeout=<seconds>`. A repeat timeout drops\n  the value suggestion and keeps the detach advice\n- a cell that ignores its interrupt is killed and marked restarted, never\n  re-run, and the kernel restarts lazily on the next cell\n- the deadline is host-side and local: no new event kind, request field,\n  capability, or `DAEMON_PROTOCOL_VERSION`/`DAEMON_SCHEMA_REVISION` change\n\nTests: repl-kernel-abort (24), ipython-provisioner (30), kernel-state-snapshot\n(3), repl-kernel-execute (10, real kernel venv: a 30s cell stopped at a 1.5s\ndeadline with partial output kept, a cell blocked in one-shot `await bash(cmd)`,\nand a 2s cell left alone when no timer is armed), and\ntest/suite/regressions/ipython-cell-deadline.test.ts (2) which asserts the\naudited defect is gone: the turn continues instead of parking.\n"},{"name":"r15-prompt-payload-ceiling.md","kind":"file","size":137,"lastCommitHash":"cd73c7651be077e59e3175d48b7df77535bc2307","lastCommitWhen":"2026-09-17T07:04:34+03:00","lastCommitMessage":"feat(coding-agent): a prompt payload ceiling and pinned model metadata (R15)\n\nModels empirically probed context limits and produced a 6.5M uncached-token\nblowout. Now:\n\n- the compiler computes a payload ceiling from the model's real context window\n  and refuses to build a larger payload, reporting what was dropped or what to\n  do rather than silently truncating mid-turn\n- model metadata (context window, max tokens, cost, reasoning) is pinned per\n  model id for the life of a session, so the per-request path no longer\n  re-derives it from a remote refresh (`refreshModelsDev` stays for discovery)\n- the effective context window and the ceiling are stated once where the model\n  can read them, instead of being something it probes for\n\nNo text was added that tells the model to compact or shorten because context is\nlong: per P1 the harness owns compaction and the numbers are measurements.\n\nTests: test/suite/prompt-payload-limit.test.ts (11); regressions re-run:\nagent-session-prompt (65), agent-session-compaction (39).\n"},{"name":"r3-wake-child-death-notices.md","kind":"file","size":250,"lastCommitHash":"6e95302bd9cfc3987e0f31c966074d21a19d7918","lastCommitWhen":"2026-09-17T07:01:44+03:00","lastCommitMessage":"fix(coding-agent): wake the parent when a subagent dies (R3)\n\nAn idle parent learned nothing when a child died: the synthesized terminal\nnotice was deferred into the pending next-turn queue and admitted with\nwake: false, so it only surfaced on the parent's next external resume. That\nstranded the parent behind 8 stillborn children (01a0a253, 01a08f07,\n01a0a1ab).\n\nAbnormal terminations now wake the parent:\n\n- failure notices and completed-without-reply notices are admitted with\n  wake: true and resumeIfIdle, so an idle parent reads them now; a\n  parent-initiated cancellation keeps the quiet path, because waking on a\n  cancel would restart the turn the parent just ended\n- a wake notice is not held back by the pump suspension that parked it, but\n  nothing is admitted during an owned admission pause or an update-restart\n  teardown; the release path re-flushes those\n- the flush removes a notice from the queue before admitting it, so the\n  re-entrant flush from a wake cannot deliver the same notice twice\n- a simultaneous swarm reads in fewer turns than deaths: the actions stay\n  queued and the pump batches them (followUpMode \"all\")\n\nFailure notices also carry an autopsy header: status, runtime, tool-call\ncount, last assistant text, and a one-line next step. The header is omitted\nwhen no autopsy fields are present, so hand-built notices keep the bare\n[child-failed] shape.\n\nTests: packages/coding-agent/test/suite/subagent-terminal-notice.test.ts\nPlus a negative control: with the wake predicate forced false the swarm test\ntakes 2 turns for 3 deaths and the suspended-parent test never wakes.\n"},{"name":"r6-r7-skills-attenuation-and-surface.md","kind":"file","size":828,"lastCommitHash":"e88e3a28a2336ad92bfe06c3725c290fb0e9d54e","lastCommitWhen":"2026-09-17T07:04:26+03:00","lastCommitMessage":"feat(runtime,skills): one calling convention, quiet attenuation, disambiguated edit, network perimeter\n\nR7 S1 (the calling convention) - the cell boundary awaited only coroutines, so a\nskill that returned a plain value made `await skill()` raise \"object X can't be\nused in 'await' expression\" (156 of those in the audit corpus). The evaluator now\nsettles whatever a call returns, `f(x)` and `await f(x)` both work, and every\neager result is settled once; `BashHandle` and a started `asyncio.Task` stay plain\nvalues on purpose, because awaiting them would silently turn `bash(cmd)` into a\nblocking one-shot.\nR7 S4/S5/S6 (runtime half) - one generated surface per skill: a line per published\ncall plus a generated shape line per result type, a byte-capped signature digest,\nand an error boundary that appends the resolved signature, the nearest legal name,\nand the correction for a wrong keyword, a missing positional, awaiting a settled\nvalue, and a wrong attribute. A call that fails the same way twice gets one\nin-band repeat notice (second occurrence, then every fifth) and is never refused.\nThe `signatures` kernel request is additive: an older host never sends it and the\nruntime falls back to indexing the wrapped modules in the cell namespace.\n\nR6 (capability attenuation) - a subagent pushed unreviewed work to origin and ran\ndestructive repository commands (Sev 5, 01a08d13, 01a09632). `git`/`jj` skills now\nrefuse a publish or a force reset/restore from a child unless the parent authorized\nit, and the refusal names the correction (`allow_push=True` at spawn, or\n`RLM_ALLOW_PUSH=1`). Depth-0 sessions are unaffected.\n\nR7 S3/S7 (skills half) - `git`/`jj` take `cwd` for the repository directory and\n`paths=` for target files (`n` -> `limit` on the log/graph calls), the fact/action\nsplit is consistent (collections iterable, shapes generated, no hand-written\nsignature text), and `RemoteHandle.output` is a property.\n\nI1 - a non-unique `old_str` in the `edit` skill now changes nothing and reports the\nbounded line ranges of every occurrence with numbered candidate snippets, and\n`line_hint=<a line inside the wanted range>` resolves it in one retry. Exact-match\nsafety is unchanged.\n\nI6 - a command that would bind a server to a public interface is refused with the\nloopback form to use instead (`RLM_ALLOW_PUBLIC_LISTEN=1` for a deliberate public\nbind), and a hardcoded dev port warns with the ephemeral-port correction. Read-only\npipelines and ssh stay permissive; the R5/R8 guards are untouched.\n\nI5 is closed as superseded by R7 S6.\n\nTests: prime-agent-runtime suite 425; git 15, jj 23, search 11, edit 13 python\ntests; new test_skill_surface.py (20) and test_subagent_workspace.py (16).\n"},{"name":"r7-calling-convention-and-signatures.md","kind":"file","size":695,"lastCommitHash":"ae6ee379908f52365f7731a4812233694fd800c5","lastCommitWhen":"2026-09-17T07:04:42+03:00","lastCommitMessage":"feat(coding-agent): publish the generated skill surface, behind a capability gate (R7 S4/S5 host half)\n\nThe runtime generates one line per published call plus a shape line per result\ntype; the host now publishes it, so a signature is stated once instead of being\nhand-written in prose that drifts.\n\n- `KernelClient.skillDigest(skills, signal)` is capability-gated: the manager\n  records the `ready` event's optional `capabilities` list (reset per start and\n  teardown) and returns null without it, so an older runtime is never asked and\n  an old host keeps working. A runtime that rejects the request also yields null,\n  never an error.\n- the `ipython` provisioner reads the digest once after the runtime bootstrap and\n  caches it, and the session rebuilds its base prompt when it arrives, so the\n  first turn after a fresh kernel carries the surface.\n- the prompt prints the digest with the \"if the digest disagrees with the\n  installed code, the code wins\" sentence in place of the inspect.signature line.\n- with the digest as the published surface, the hand-written signature prose it\n  replaces is gone: the `search` description signature, the `git` `cwd=`/`n=`\n  examples, the `nix` handle-API list, and the `bash()` API half of the prompt\n  paragraph (R14's `timeout=` sentence, I1's `line_hint=` sentence and I6's\n  public-listen sentence stay).\n- docs/rlm-runtime.md lists the `signatures` request and the capability gate.\n\nAlso fixed: test/ipython-bootstrap.test.ts still asserted the\n`async def roles(self, role=None)` stub that role routing removed when\n`rlm.models()` discovery replaced it; it now asserts the stubs the prompt calls.\n\nTests: new test/repl-kernel-skill-digest.test.ts (4 fake-protocol kernel cases:\ncapable runtime asked with the right names, no-capability runtime null and\nunasked, capable runtime rejecting, empty list and stopped kernel),\nipython-provisioner (32), repl-kernel-execute (11, real kernel digest carries\n\"# skill surface\"), system-prompt (29), repl-kernel-abort (24). The capability\ngate has a negative control: sending the request without checking the capability\nfails the \"unasked\" case.\n"},{"name":"r7-s8-skill-metadata-lint.md","kind":"file","size":281,"lastCommitHash":"cae335999a0bf48e24acc98bc7aeb97099c7dbf4","lastCommitWhen":"2026-09-17T07:05:21+03:00","lastCommitMessage":"feat(coding-agent): the skill metadata lint that keeps signatures out of descriptions (R7 S8)\n\nThe generated surface is now the only place a signature is published, so the\nloader warns when a skill restates one:\n\n- skill identity is the directory name; a frontmatter `name` that disagrees is\n  reported and never used as the identity\n- a body whose first non-empty line is not a heading naming the skill warns\n  (case and separators are folded, and a qualifier after the name is allowed,\n  because the bundled convention is `# Title Case`)\n- a description containing a call signature (`name(...)` with arguments, or\n  `await name(`) warns with the correction: signatures belong in the generated\n  surface\n\nWarnings only: the missing-description drop, every existing diagnostic, and the\nresolved skill set are unchanged.\n\n`websearch/SKILL.md` was the one bundled skill whose description carried four\n`await websearch.*(...)` call forms; it now describes the call without the syntax.\n\nTests: test/skills.test.ts (36, four new plus \"every bundled skill directory\nloads\"), builtin-skills.test.ts (22), system-prompt (29). Negative controls:\nstubbing the signature check fails only its test; stubbing the title check fails\nonly the two title tests.\n\nNot done, deliberately: prime-agent-runtime/src/rlm/harness.py duplicates the\nharness-state call-contract paragraph, but the python copy is an abridgement\n(different clauses, different quoting), so aligning them cannot keep both\nrenderings identical - that is a consolidation, not a lint fix.\n"},{"name":"r7-settled-skill-results.md","kind":"file","size":382,"lastCommitHash":"7446965d9b14bff5fda33eb27259015d5b9c4dff","lastCommitWhen":"2026-09-17T07:04:42+03:00","lastCommitMessage":"feat(skills): settled skill results, so `await` on a finished call works (R7 S2)\n\nThe runtime settled the trailing cell value and the `rlm.*` handles, but a skill\ncall still returned a plain str/list/dict, so `await git.root()` raised \"object\nstr can't be used in 'await' expression\" - 156 of those in the audit corpus.\n\n- `rlm.skill` gains `settled_call`/`settle_module`/`_settle_result`; each skill\n  module settles its published callables at import through a soft `rlm.skill`\n  import, so a skill still works as a plain CLI without a kernel\n- `None` stays `None` deliberately: awaiting it still raises loudly rather than\n  turning an absence into a value (the design's `Missing` type is not introduced)\n- the five local `Awaitable*` classes and thirteen `__await__` definitions are\n  gone; result subclasses inherit `SettledList` (with a `list` fallback) and keep\n  their own type and repr, so `type(await git.log()) is git.CommitList`\n\nI2 (the remaining half): `path=` is kept in the signatures only to fail loudly\nwith both corrections (\"cwd= is where git runs and paths=[...] are the target\npaths\") on git.log/graph/diff/changed/add/commit/checkout/reset/restore and the\njj equivalents. It used to mean the run directory, so aliasing it to `paths=`\nwould have silently filtered the wrong thing; `cwd=<a file>` raises a GitError\nthat names `cwd=` and points at `paths=[...]`.\n\nTests: prime-agent-runtime 430 OK (test_skill_surface 25); git 17, jj 25,\nsearch 24, nix 9, repo 3 (new module). Negative control: dropping git's settle\nline makes the await test fail with the original TypeError.\n"},{"name":"README.md","kind":"file","size":254,"lastCommitHash":"b5807b6f932ae3b208bc190176bc04a72126ff23","lastCommitWhen":"2026-08-20T10:01:49-07:00","lastCommitMessage":"Changelog fragments: conflict-free changelog entries (#1589)\n\n* feat(coding-agent): add changelog fragments with CI check and release-time aggregation (ENG-5408)\n\n* refactor: flag-day cutover — remove [Unreleased] sections entirely\n\nPer review: drop the transition machinery. All four changelogs lose their\n[Unreleased] header; coding-agent's pending entries move into a fragment.\nrelease.mjs no longer re-adds the header; the CI check accepts only\nfragments (or the no-changelog label). The stray-[Unreleased] absorption\nstays in buildReleaseSection so an accidentally merged old-style entry is\nfolded into the release instead of stranded. Test file removed.\n\n* fix: address bot findings — escape version in dry-run regex, refuse empty fragments\n\nCodeQL: the dry-run preview regex embedded the CLI version argument\nunescaped. Macroscope: empty fragments were git rm'd without appearing in\nthe changelog; the release now aborts on them and the CI check requires\nthe added fragment to have content. Cursor: normalizeFragment unexported.\n\n* fix(ci): verify fragment content, not just line additions\n\nA whitespace-only fragment passed the additions>0 guard but aborts the\nrelease. The check now reads each candidate fragment's content via its\ncontents_url (head-repo scoped, so fork PRs work) and requires non-blank\ntext.\n\n* refactor: decouple CI from release semantics for empty fragments\n\nThe fail-closed release abort forced CI to predict exactly what release\nwould reject, which pulled content fetching and fork-ref handling into\nthe workflow. Empty fragments have nothing to lose, so the release now\nwarns and leaves them unconsumed instead of aborting; CI goes back to a\nplain added-file presence check. Also trims comments and doc blocks.\n\n* fix(ci): fail closed when a PR exceeds the listFiles cap\n\nAbove 3000 changed files the API cannot return the full list, so the\ncheck could miss a src change. The no-changelog label remains the\nexplicit escape hatch."},{"name":"rebase-auth-precedence.md","kind":"file","size":367,"lastCommitHash":"12624e0ead87ff8d4e5147a5d58987f53c14b923","lastCommitWhen":"2026-09-17T09:45:21+03:00","lastCommitMessage":"fix(coding-agent): close the scattered failures and settle the credential precedence\n\n- an explicitly stored Prime Inference key now wins over the Prime CLI\n  config; the CLI bridge stays the fallback for sessions that never\n  stored one, which is what the trace upload path and the fail-closed\n  rule both assume\n- session listing keeps a deterministic order when transcripts share a\n  modification time (results are collected in directory order instead of\n  completion order) and the in-memory retention tests pin the catalog off\n- a retry policy with maxRetries 0 no longer enters the quota wait\n- the autonomous, package-path, trace, quota and refinement suites mirror\n  the fork's contracts: the spawn gate and named model, daemon env\n  isolation, an async host request, and a digest ranking test that asks\n  for the detailed tier only (the shipped default also lists stubs, which\n  the neighbouring cases cover)\n"},{"name":"remote-kernel-execution.md","kind":"file","size":263,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"remote-python-host.md","kind":"file","size":213,"lastCommitHash":"21cdadfc1e70a98a89006fa2b4b53d65e2b2be98","lastCommitWhen":"2026-09-17T06:02:59+03:00","lastCommitMessage":"feat(coding-agent): add remote python host support and ui indicators\n"},{"name":"remote-reflection.md","kind":"file","size":90,"lastCommitHash":"0c098fbfbe4f85ac995bd79fb6f78fc59f946779","lastCommitWhen":"2026-09-17T06:20:35+03:00","lastCommitMessage":"coding-agent/{skills/remote,main},runtime/remote: reflect remote apis\n"},{"name":"remove-google-antigravity-example.md","kind":"file","size":88,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"resume-cross-project-in-place.md","kind":"file","size":140,"lastCommitHash":"8ed53d811dd26299c4122c0bf5c1dd694c533430","lastCommitWhen":"2026-09-18T03:07:37+03:00","lastCommitMessage":"coding-agent: resume another project's session where it recorded itself\n"},{"name":"rlm-models-api.md","kind":"file","size":175,"lastCommitHash":"870b30c1290f25514aa318f990335f9f841c7359","lastCommitWhen":"2026-09-17T06:56:04+03:00","lastCommitMessage":"feat(coding-agent): replace role routing with rlm.models discovery and gate\n"},{"name":"role-based-subagent-routing.md","kind":"file","size":233,"lastCommitHash":"9286377a734f0f527197bea6f8ca2e425a638ae1","lastCommitWhen":"2026-09-17T06:34:40+03:00","lastCommitMessage":"feat: route rlm subagents by role instead of a model selector\n"},{"name":"runtime-command-guard.md","kind":"file","size":747,"lastCommitHash":"4d31239144b3ca825c2725d53ab35da926e33616","lastCommitWhen":"2026-09-17T07:02:56+03:00","lastCommitMessage":"feat(runtime): scope kills, allow read-only pipelines, inherit the developer env (R5, R8, R9)\n\nThree defects in the same command layer, fixed together because they touch the\nsame functions.\n\nR5 - a `pkill` in a subagent's cleanup killed unrelated processes (01a09d7f):\nthe guard now refuses kill, pkill, and killall forms that can reach outside the\ncommand's own process group, including kills hidden behind sudo/env prefixes,\nxargs, timeout, shell -c, subshells, pipe stages, and the remote payload of\nssh/mosh. Narrow scopes (`pkill -P $$`) stay allowed, and `_signal_group`\nrefuses a pid that is not its own group leader so a group kill can never land\non the daemon or an ssh ControlMaster.\n\nR8 - the guard rejected standard read-only pipelines (`... | head`) with an\nopaque ValueError: read-only stages (head, tail, grep, wc, jq, sort, uniq) now\npass, a mutating or unknown stage still refuses the whole command, and every\nremaining refusal names the correction instead of only the rule.\n\nR9 - swarm builds died with exit 127 `cargo: command not found` (01a09746):\n`_child_env` now inherits the launcher's environment verbatim, with the POSIX\ndefault utility path when no PATH is supplied. No login-shell probe, no rc\nfile, no new subprocess.\n\nTests: prime-agent-runtime test.test_bash 101 (14 new), the 13-module runtime\nsuite 382 (baseline 353). Negative controls for each new behaviour are recorded\nin the child's report: kill scanner off, group-ownership check forced permissive,\nread-only filter emptied, write detection off, PATH floor removed.\n"},{"name":"scheduled-jobs-artifact-index.md","kind":"file","size":222,"lastCommitHash":"55815475a01226b30a42a1e07886d24798422d6e","lastCommitWhen":"2026-09-17T06:42:40+03:00","lastCommitMessage":"coding-agent/modes/daemon: index scheduled jobs by session artifact dir\n"},{"name":"search-cite.md","kind":"file","size":257,"lastCommitHash":"8fd451b1ff82f28174d699d390f7bd40c890478a","lastCommitWhen":"2026-09-17T07:04:42+03:00","lastCommitMessage":"feat(skills,coding-agent): grounded citations and the isolated-workspace suite test (I4, R12)\n\n- `search.cite(path, line, end_line=None)` returns a content-hashed snippet\n  (`Citation(path, line, end_line, snippet, hash, algorithm)`), so a citation can\n  be verified later by re-hashing. Read-only, one file, bounded (40 lines / 8 KB),\n  and a past-end, reversed, blank or oversized range fails with the correction\n  rather than returning a partial snippet. CRLF and a missing final newline\n  normalise to the same snippet and hash.\n- test/suite/subagent-workspace-colocation.test.ts asserts at the host boundary\n  that an isolated spawn's checkout path reaches `CreateRlmSubagentRuntimeOptions.cwd`\n  (absolute, under `.worktrees/`, distinct from the parent cwd) and that an unknown\n  `workspace` value is refused with its correction before any allocation.\n- `rlm.spawn`'s namespace facade now forwards `allow_push` too: without it the\n  generated surface carried two `spawn` declarations, so the nearest-name remedy\n  read `(rlm.spawn, rlm.spawn)`. Two surface snapshots were updated for the\n  signature they legitimately describe.\n\nTests: prime-agent-runtime 425 OK; search 23 (12 new); the two new suite tests\nrun 4/4 with subagent-cwd-propagation.test.ts.\n"},{"name":"session-catalog-db.md","kind":"file","size":349,"lastCommitHash":"bc83035497589681822e48a0ffc5642a9fc2b86f","lastCommitWhen":"2026-09-17T06:57:23+03:00","lastCommitMessage":"coding-agent: index session summaries in a sqlite catalog\n\nListings no longer rescan every transcript after a daemon restart: a sqlite\ncatalog keyed by canonical path and file identity (dev, ino, size, mtime)\nanswers from stored summaries, and an appended file resumes from the persisted\nbyte offset and scan accumulator.\n\nMeasured on 300 sessions / 595MB: cold listing after a restart 2.5s -> 0.26s,\nwarm 0.29s -> 0.18s. The daemon answers listings itself when the catalog is\nlive (PRIME_AGENT_SESSION_CATALOG_DB=child keeps the catalog child process,\n=0 disables the catalog entirely); the store degrades to in-process scanning\nwhenever sqlite is unavailable, including under bun through bun:sqlite.\n"},{"name":"session-list-stream-only.md","kind":"file","size":174,"lastCommitHash":"17271d3a5e7887de090ac69c4dd7cdca2c995f1a","lastCommitWhen":"2026-09-17T06:59:15+03:00","lastCommitMessage":"coding-agent/daemon: stream saved sessions once\n\nA listing sent every row twice: once as session_list_item progress messages and\nagain in the response. Clients that advertise session_list_stream_only now ask\nfor `streamOnly`, and the daemon answers with an empty array because the streamed\nitems are the whole list.\n\nHalves the listing payload: 18.4MB -> 9.2MB and 233ms -> 119ms for 300 sessions\non a 595MB corpus, and 184ms -> 72ms warm through the real client path. Daemons\nthat do not advertise the capability are unaffected, and the response still\ncarries the full array for clients that never send the flag.\n"},{"name":"session-recap-toggle.md","kind":"file","size":176,"lastCommitHash":"ba0ddb1062a6f6de20c25a892addeca7092d9fe7","lastCommitWhen":"2026-09-17T20:24:08+03:00","lastCommitMessage":"coding-agent: add a sessionRecap.enabled off switch for the daemon status recap\n"},{"name":"session-search-deadline-reset.md","kind":"file","size":245,"lastCommitHash":"b2ea96f2fa7ab68f22c2735f6130e7196b754e6c","lastCommitWhen":"2026-09-17T22:39:15+03:00","lastCommitMessage":"coding-agent: clear the session search deadline when a roster miss resolves\n"},{"name":"session-selector-fast-path.md","kind":"file","size":239,"lastCommitHash":"90dc81bdde8eb3fc2fdaf271cbe811ac20f5704c","lastCommitWhen":"2026-09-17T06:41:46+03:00","lastCommitMessage":"coding-agent/core: resolve session selectors from the transcript file name\n"},{"name":"session-tree-cold-path-perf.md","kind":"file","size":335,"lastCommitHash":"95ef548f7ca66b4909bf15185ac1274150a2f6b5","lastCommitWhen":"2026-09-17T06:57:23+03:00","lastCommitMessage":"coding-agent/modes/agent-connection: project the session tree over the wire\n"},{"name":"ssh-control-stdin-piping.md","kind":"file","size":259,"lastCommitHash":"8bece54b7cfafdcc8f5bcb8fff7bfa11fddfd324","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"coding-agent/skills/ssh-control: add stdin piping to run and dead-master re-ensure guidance\n"},{"name":"ssh-exempt-spill-output.md","kind":"file","size":160,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"subagent-brief-composition.md","kind":"file","size":171,"lastCommitHash":"5eed86fbbfda1ca3d93d14040db092dc242f5da4","lastCommitWhen":"2026-09-17T10:28:52+03:00","lastCommitMessage":"feat(rlm): compose invariant child brief sections and warn on unlinted briefs\n"},{"name":"subagent-cwd-reply-and-message-backpressure.md","kind":"file","size":538,"lastCommitHash":"2de5e72f0ad04bbdbde470ea9eb57907ec80d14d","lastCommitWhen":"2026-09-17T07:03:07+03:00","lastCommitMessage":"fix(coding-agent): pin the child cwd, harvest silent children, backpressure fan-in (R4, R11, R10, R9)\n\nR4 - children defaulted to whatever the host happened to pass, so recursive\nsearches walked multi-TB caches and froze kernels for 21 and 92 minutes\n(01a09d7f, 01a09bf6, 01a09351). `CreateRlmSubagentRuntimeOptions` now carries\nan explicit `cwd`, the parent always sets it to its own session directory, and\nboth runtime hosts and the inline path use it instead of reaching into the\nparent session. Proof: test/suite/subagent-cwd-propagation.test.ts.\n\nR11 - 11 consecutive children exited without replying (01a09782) while parents\nwaited on fan-in that never came. A child that finishes with zero\n`agent_message.send` calls now hands its last assistant text to the parent as\n`[auto-harvested fallback from <name>]`, tagged as unreviewed, with an empty\nchild still getting the plain no-reply notice. The reply obligation also moved\ninto the task envelope and the child doctrine, where a child cannot miss it.\nProof: test/suite/subagent-reply-harvesting.test.ts.\n\nR10 - fan-in hit a hard 20-pending ceiling and threw at the exact moment\nresults arrived (01a09d35). The ceiling is now soft (200) and admission never\nfails on it: the message buffers in memory, and the daemon yields up to 30s for\na saturated recipient to drain before delivering anyway. The throwing\n`assertAgentMessageQueueCapacity` helper is gone; the two tests that asserted\nthe old fatal cap now assert the resilient behaviour. Proof:\ntest/suite/agent-message-backpressure.test.ts (35 concurrent senders, zero\nrejections).\n\nR9 (host half) - the worker forwarded PATH alone to a session it created, so\n`cargo`/`nix` were missing under a GUI- or launchd-started daemon (01a09746\nexit 127). The worker now forwards the launching shell's environment, which the\nsupervisor already seeded it with.\n"},{"name":"tool-duration-for-model.md","kind":"file","size":63,"lastCommitHash":"f1c920f48c3f427204896d6f9ee8dea1520e2b2d","lastCommitWhen":"2026-09-17T05:54:07+03:00","lastCommitMessage":"feat(coding-agent): include tool call duration in results sent to the model\n"},{"name":"tool-duration-header.md","kind":"file","size":100,"lastCommitHash":"f6e10088494d4d8d2bee79210a3c59188eb64f09","lastCommitWhen":"2026-09-17T05:54:07+03:00","lastCommitMessage":"feat(agent,coding-agent): record tool call durations and show them in the tool header\n"},{"name":"tool-ergonomics.md","kind":"file","size":385,"lastCommitHash":"3e658fe345c57401621e918602713d5aa8a01610","lastCommitWhen":"2026-09-17T12:11:29+03:00","lastCommitMessage":"rlm,ssh: unify handle and result text accessor and add accountable stdin and copy verification\n"},{"name":"unrestorable-session-search-bound.md","kind":"file","size":184,"lastCommitHash":"7d73c1026db8f70b7d475fe889633268fdcdf5e2","lastCommitWhen":"2026-09-17T14:31:07+03:00","lastCommitMessage":"fix(coding-agent): bound the daemon session reattach wait and report an unrestorable session\n"},{"name":"update-restart-dead-worker.md","kind":"file","size":128,"lastCommitHash":"669b094faa70e6da36b98b912bef6a4e74333e80","lastCommitWhen":"2026-09-17T07:05:48+03:00","lastCommitMessage":"fix(coding-agent): never let a provably dead resident worker block an update restart\n\nA session worker whose process was gone could keep its registration resident\nforever: adoption cannot relaunch it (the relaunch command is only recorded for\nclient-owned workers), so recovery parks it as failed waiting for \"a client with\nfresh runtime context\", and the only reclaim path runs when its session is opened\nagain — which never happens once that session file is deleted. It is in no roster,\nso `stop` cannot name it and `doctor --fix` does not see it, yet\n`prepareUpdateRestartFenced` refuses to hand over while any resident worker is not\nready or has no client. The updater installed the package and then kept running the\nold daemon forever, with no supported way out.\n\nThe update restart now retires the workers it provably cannot hand over, before\ncomputing the acknowledgement set:\n\n- provably dead means a recorded pid whose process is gone or recycled; a\n  descriptor without a pid stays unprovable and still refuses the restart, so an\n  unreachable or recovering worker keeps its registration.\n- a supervisor-owned corpse is reclaimed with the same cleanup a later open runs,\n  and its session stays on disk for the next open.\n- a client-owned corpse is only skipped in this pass, so its owner still finds the\n  registration and the session.\n\nRecovery keeps its pinned fail-closed behaviour: a worker whose pid was reused\nwithout fresh runtime context is still parked, not reclaimed, which the new test\nsuite pins alongside the two refusals.\n\nSuite: 1090 pass, #502 pre-existing; a clean worktree at origin/main fails more\n(3 files) under the same sweep, so the remaining single flake is environmental.\n"},{"name":"update-restart-failure-status.md","kind":"file","size":81,"lastCommitHash":"f712dd1d4be7767f5951957312f00568afa7961b","lastCommitWhen":"2026-09-17T06:25:27+03:00","lastCommitMessage":"coding-agent/update: fail closed and identify prepare timeouts\n"},{"name":"update-restart-marker-order.md","kind":"file","size":159,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"usage-query-layer.md","kind":"file","size":147,"lastCommitHash":"da44bb76b40cc5a6087934b9631c767e573bdc7c","lastCommitWhen":"2026-09-17T07:08:37+03:00","lastCommitMessage":"refactor(ai,coding-agent): move the usage query into the provider layer behind a JSON contract\n\n/usage could only ever report one provider because the query lived inside a TUI file:\nthe endpoint, the JWT claim reading, the window parsing, and the rendering all sat in\ncoding-agent, and the renderer named Codex in strings. dawn wants that query from a\nmacOS app and elsewhere, so the query half moves to the package that already owns\nproviders and credentials (@earendil-works/pi-ai, in-repo) and becomes host-neutral.\n\n- packages/ai/src/utils/usage/: `UsageLimit`/`UsageReport` (now carrying\n  `providerName`), `UsageCredentialEntry`, and `collectUsage(entries, opts)` returning\n  `{ reports, skipped }`. Credentials are passed IN, so a host with its own token needs\n  no auth.json.\n- `OAuthProviderInterface.usage?()` is the extension point, implemented in\n  openai-codex.ts by moving the endpoint, JWT helpers, parse, and fetch there. Adding a\n  provider to /usage is now one file, with no registry edit; a provider without the hook\n  lands in `skipped` by name instead of being silently dropped.\n- coding-agent keeps rendering + the auth-store bridge. Headings come from\n  `providerName`/display names, and the empty state names providers that have no feed\n  rather than claiming Codex has none.\n- `prime-agent usage [--json]` is the language-neutral face: stdout is exactly one JSON\n  object, the documented contract, and a provider failure is data (`error`) rather than a\n  non-zero exit. That contract is the port target for a native host.\n- oauth dynamic node built-in imports move to the house `dynamicImport(\"node:\" + ...)`\n  pattern: re-exporting the usage module links getOAuthProvider into the browser bundle,\n  and check:browser-smoke fails without it.\n\nVerified: pi-ai usage tests 7/7 with injected fetch (no network), coding-agent usage\ntests 40/40, `npm run check` clean, and a live run against the stored codex accounts\n(two accounts reported, `skipped: [\"anthropic\", \"google-antigravity\"]`). The 13 failing\npi-ai test files are pre-existing: a baseline worktree at the previous head fails the\nsame 13 with 676 tests vs 683 here, the difference being the new 7. Negative control:\nthrowing in `parseUsagePayload` fails the adapter tests immediately.\n"},{"name":"wake-unobserved-bash-completions.md","kind":"file","size":90,"lastCommitHash":"03c3ca484fbaa7f610a185df65fd73b05599651a","lastCommitWhen":"2026-09-17T05:52:15+03:00","lastCommitMessage":"feat(coding-agent,runtime): rebase minimal runtime extensions on upstream 0.9.3\n"},{"name":"worker-exit-attribution.md","kind":"file","size":139,"lastCommitHash":"9e09089b2ac775e427e112e948d2aca2d3b55043","lastCommitWhen":"2026-09-17T22:18:11+03:00","lastCommitMessage":"fix(coding-agent): attribute worker exit reasons and defer heartbeat stops for busy workers\n"}]