Skip to content

refactor(frontend): remove phone-home and Autofix from the dashboard - #57

Merged
patrickbrandao merged 6 commits into
mainfrom
feat/13-remove-phonehome-autofix-ui
Oct 11, 2026
Merged

patrickbrandao merged 6 commits into
mainfrom
feat/13-remove-phonehome-autofix-ui

Conversation

@patrickbrandao

@patrickbrandao patrickbrandao commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Closes #13

Stacked on #51 (feat/12-remove-saas-dashboard, issue #12). The base is that branch, so this diff shows only #13. Retarget to main once #51 merges.

What changes

The dashboard stops making outbound or Manifest-hosted calls of its own and drops the Autofix UI. This PR is frontend only. The backend modules go in #16 (Autofix) and #17 (telemetry, version, discovery, Sentry, Wingman/CORS).

  • Update check: VersionIndicator only renders v{__MANIFEST_VERSION__}. vite.config.ts injects that constant at build time from the canonical packages/manifest/package.json. The component makes no request, has no "new version" link to the manifest.build changelog and keeps no dismiss state. services/api/version.ts is deleted.
  • Discovery: the post-signup questionnaire is deleted: the page, services/discovery.ts, discovery.css, the /discovery route and its tests. The pending-discovery redirects in AuthGuard and GuestGuard are gone, and AuthGuard no longer needs its routeChecked signal.
    • Setup: after creating the first admin, it signs in and goes to /, which redirects to /overview. If auto sign-in fails, it goes to /login.
    • Register (email and social): goes to /welcome, the step that used to follow Discovery.
    • A leftover manifest_discovery_pending_* key from an older build no longer redirects anyone. Tests cover this.
  • Wingman: deleted WingmanDevTools, wingman-dev-proxy.ts and its Vite plugin, VITE_WINGMAN_*, __WINGMAN_URL__ and __DEV_MODE__. Also gone: the mounts in App and Welcome, the drawer CSS and the Wingman section of CONTRIBUTING.
  • Autofix: deleted AutofixKpiCards, SettingsAutofixSection and NotificationBell, whose only notification was "Autofix is inactive". Also removed:
    • the Autofix switch and the Manifest Terms/Privacy line at harness creation (AddAgentModal and Welcome);
    • in the Requests views: the autofix badges, the trigger-filter option, the Phoenix panels, the sibling links, the Autofix: … status and the Autofixed label;
    • the "Autofixed attempts" connection card, the "Doctor" tooltip variant and the healed chart series;
    • the autofix fields and clients in services/api/*;
    • public/autofix-icon-email.png, public/banner-autofix-gradient.png and the dead CSS.
  • Kept: the generic reliability metrics, which the backend still serves under the legacy /overview/autofix-* paths: success rate, fallback recoveries, request status over time, and per-harness/provider/model reliability.
    • The frontend reads them through neutral clients (getReliabilityStats, getRequestStatusTimeseries, getPer*Reliability), and the paths live in one RELIABILITY_PATHS constant.
    • ReliabilityKpiCards shows four cards: Success rate, Recovered requests, Recovered by Fallback, Failed requests.
    • "Self-healed" becomes "Recovered" and counts fallback only.
  • Temporary legacy Autofix opt-out (until Remover Autofix/Phoenix do backend #16): a harness stored without autofix_enabled inherits the backend default. That default is ON whenever the backend detects no container (bare-metal npm start, dev runs), and the dashboard no longer has a switch to turn it off. So, in one commented block in services/api/agents.ts:
    • createAgent always sends autofix_enabled: false. This covers the Connect Harness modal, Welcome and ConnectProvider.
    • duplicateAgent follows the copy with PATCH /routing/:agent/autofix {enabled:false}, because the duplicate endpoint takes no such field.
    • For harnesses created before this, or through the API/MCP/CLI, the dev guide documents AUTOFIX_GLOBAL_ENABLED=false as the operator workaround (or MANIFEST_MODE=selfhosted).
  • Legacy healed series: a pre-Remover Autofix/Phoenix do backend #16 backend can still return a healed key in the request-status timeseries. The client folds it into success, because the backend's success rate already counts those requests as successes. That way:
    • the bars still sum to the request total;
    • Success + Recovered + Error matches the Success rate card;
    • Recovered stays fallback-only, like its KPI.
  • Stale ?trigger=: on load, the Requests page rewrites an unknown or legacy ?trigger= value (for example ?trigger=autofix) to the filter it actually applies.
  • Regenerated the design-system registry and updated the inherited dev guide.
  • No changeset: user-facing changelog entries are deferred to the 0.1.0 release (Release, Changesets 0.1.0 e publicação no Docker Hub #31), as in refactor(frontend): remove the SaaS layer from the dashboard #51.

Acceptance grep

grep -rniE "autofix|healed|wingman|/discovery|version-check|updateAvailable" packages/frontend/src matches only three documented, temporary legacy blocks, all removed by #16:

  • services/api/analytics.ts: the RELIABILITY_PATHS block (five backend paths) and the healed fold;
  • services/api/agents.ts: the Autofix opt-out.

These are generic reliability endpoints the dashboard still needs. The backend exposes them only under these names, and backend changes are out of scope here. When #16 deletes autofix-stats.service.ts, it must:

  • keep these metrics on neutral paths;
  • update this one constant and delete the other two blocks.

Otherwise the Overview KPIs, the request-status chart and the reliability columns break.

Gates (local, until #20)

Check Result
npm run lint (frontend) 0 errors, 49 warnings (pre-existing no-unused-vars / no-explicit-any)
tsc --noEmit -p packages/frontend/tsconfig.json pass
vite build pass
vitest run (local TZ, UTC-3) 200 files, 4137 tests passed
TZ=UTC vitest run 200 files, 4137 tests passed
node scripts/check-design-tokens.mjs clean
npm run design:registry:check and node --test scripts/design-registry.test.js up to date, 14/14

In the built bundle:

  • grep -E "wingman|blue\.manifest|/api/v1/version|telemetry\.manifest" finds nothing.
  • autofix appears only in the five overview/autofix-* paths.

How to verify

  1. On an empty database, open /setup and create the admin. It should land on /overview with no /discovery step.
  2. Sign out, open /register (when signup is enabled) and create an account. It should land on /welcome with no /discovery step.
  3. On Overview, check the four reliability cards and the chart's "Recovered requests" tab. Neither mentions Autofix.
  4. On Requests (/messages), the attempts filter offers only With a fallback attempt and No recovery attempt. An old ?trigger=autofix,fallback link filters by fallback and rewrites the URL to ?trigger=fallback. A ?trigger=autofix link is cleared.
  5. Harness Settings and the Connect Harness modal have no Autofix switch and no manifest.build link. In DevTools, the POST /api/v1/agents body carries autofix_enabled: false. A duplicate is followed by PATCH …/routing/<copy>/autofix.
  6. In DevTools Network, the corner version badge makes no request.

Still pending

  • qa-docker: the issue's HAR/Playwright run over Login, Setup, Overview, Messages, Settings and harness creation, checking that the browser calls no host other than the instance. This must pass before the PR leaves draft.

🤖 Generated with Claude Code

patrickbrandao and others added 2 commits October 10, 2026 15:21
Thinker is self-hosted only, so the inherited Manifest Cloud UI goes.
The frontend leaves first so no screen breaks when #14/#15 drop the
matching backend endpoints.

- Billing/plans: delete PlanPicker, UpgradeSuccessModal,
  UsageLimitBanner, the /upgrade page, the Register ?step=plan picker,
  the plan stores, the plan-based range locks on the analytics pages,
  the plan-limit upgrade CTAs in the request views, the Account billing
  and usage-email sections, and the @better-auth/stripe client plugin
  and dependency.
- API Bot: delete the banner, the sidebar card, the dithered ground,
  their stylesheet and the SVG asset.
- GitHub stars: drop the header star button and its /api/v1/github call.
- Free Models: delete the page, its route, its API client and the assets
  only it used.
- Managed Gemini Free: drop the provider entry, icon alias, background
  /ensure provisioning call and the Calendly key URL.
- Cloud branches: checkIsSelfHosted() is gone and every screen behaves
  as self-hosted (Local providers always listed, local logs copy, CLI
  login pinned to this host, discovery after signup and setup). Drop the
  Manifest Cloud origin mapping, the cloud email card, the
  VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it.
- Help: drop the Manifest Calendly booking link.
- Remove the dead CSS for all of the above, update the tests and the
  inherited dev guide.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thinker makes no outbound calls of its own, and Autofix depended on
Manifest's hosted Phoenix service. The dashboard drops both before the
backend modules go in #16 and #17.

- Update check: VersionIndicator now only shows the build-time
  __MANIFEST_VERSION__ badge. It makes no request, shows no "new version"
  link to the manifest.build changelog and has no dismiss state.
  services/api/version.ts and its test are gone.
- Discovery: delete the post-signup questionnaire (page, service, styles,
  route, tests) and the pending-discovery redirects in AuthGuard and
  GuestGuard. First-admin setup now lands on / (Overview), signup on
  /welcome, and social signup also goes to /welcome. A leftover
  manifest_discovery_pending_* key from an older build no longer
  redirects anyone.
- Wingman: delete the dev drawer, wingman-dev-proxy.ts, the
  VITE_WINGMAN_* / __WINGMAN_URL__ / __DEV_MODE__ build constants, the
  App and Welcome mounts, the drawer CSS and the CONTRIBUTING section.
- Autofix: delete AutofixKpiCards, SettingsAutofixSection,
  NotificationBell (it only nagged about inactive Autofix), the Autofix
  switch and Manifest Terms/Privacy line on harness creation (modal and
  Welcome), the autofix badges, trigger filter, Phoenix panels, sibling
  links and status labels in the Requests views, the "Autofixed attempts"
  connection card, the Doctor tooltip variant, the healed chart series,
  the autofix fields in services/api/*, the two public PNGs, and the
  dead CSS.
- The generic reliability metrics that the backend still serves under
  the legacy /overview/autofix-* paths (success rate, fallback
  recoveries, request status over time, per harness/provider/model
  reliability) stay. They are now read through neutral clients
  (getReliabilityStats, getRequestStatusTimeseries, RELIABILITY_PATHS)
  and shown by ReliabilityKpiCards. "Self-healed" becomes "Recovered" and
  counts fallback recoveries only.
- Update the tests, the design-system registry and the inherited dev
  guide.

Closes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao

Copy link
Copy Markdown
Owner Author

Revisão: PR #57 (#13, remover phone-home e Autofix do dashboard)

Escopo: git diff origin/feat/12-remove-saas-dashboard...071ae495dc9662f6bf4d40c2fc0f9b0d322d7c54, um commit e 97 arquivos, só frontend, docs e registro do design system. Li o código ao redor nesse SHA, inclusive os controllers do backend que o dashboard chama.

Não há blocker. O veredito é risco alto pelo critério de auth (detalhes abaixo).

Critério de alto risco que se aplica

  • Muda auth/sessão. O PR altera os guards de sessão (AuthGuard, GuestGuard) e os destinos pós-autenticação de Setup e Register, inclusive o callbackURL do login social. Pela leitura abaixo, nada disso enfraquece a autenticação. Mesmo assim, pela ADR 0002 o merge fica com o mantenedor.
  • Nenhum outro critério se aplica: o PR não tem migration, não muda o escopo de tenant, não toca governança nem workflows e não é release.

1. Fluxo de auth: sem bypass e sem loop

  • AuthGuard.tsx:11-21: os filhos só renderizam com !isPending && session().data. Sem sessão, o guard navega para buildLoginRedirect(pathname, search). Tirar o routeChecked só elimina o gate extra do Discovery, e nada protegido aparece antes do redirect. Isso tem teste: "does not render children while redirecting to login" e "preserves the protected path and query".
  • /cli/auth e /consent continuam montados direto em AuthGuard (index.tsx:139-144). Antes eles eram exceção ao redirect do Discovery. Agora não existe redirect algum, então funcionam com qualquer sessão autenticada, e sem sessão vão para /login?redirect=…. O caminho do OAuth assinado em GuestGuard (signedOAuthDestination → location.assign) ficou intacto e tem teste.
  • GuestGuard.tsx: só saiu o desvio para /discovery. A checagem de needsSetup e o getAuthDestination (que valida redirect com isSafeInternalRedirect) continuam iguais.
  • Setup → window.location.href = '/' → App/AuthGuard → RootRedirect → /overview. Se o auto sign-in falhar, vai para /login, e o GuestGuard vê needsSetup=false porque o admin já foi criado. Não há loop.
  • Register (e-mail, token, reenvio de verificação e social) → /welcome, que fica sob WelcomeLayout = AuthGuard. Sem sessão (por exemplo, com verificação de e-mail pendente) vai para /login?redirect=%2Fwelcome. O parâmetro signup=1 só era lido por Discovery.tsx, que foi apagada, então não há consumidor órfão.
  • Uma chave manifest_discovery_pending_* que tenha sobrado não redireciona mais ninguém. Há testes para isso em AuthGuard, GuestGuard, Setup e Register.

2. RELIABILITY_PATHS contra o backend

Conferi analytics/controllers/autofix-analytics.controller.ts e analytics/services/autofix-stats.service.ts nesse SHA:

  • Os cinco caminhos de analytics.ts:282 existem como GET com @TenantCtx() e escopo por tenant (addTenantFilter). O dashboard não chama mais autofix/status nem autofix/enable-all.
  • ReliabilityStats (success_rate, fallback_saves, total_requests, errors_remaining, todos {value, previous}) é um subconjunto exato de AutofixStatsResponse. Os campos que o backend ainda manda (autofix_saves, coverage, dispositions, needs_attention) são ignorados sem quebrar nada.
  • getRequestStatusTimeseries manda by=disposition, que está em AUTOFIX_TS_DIMENSIONS. Para disposition, o backend devolve as chaves success/healed/fallback/error (request-volume.service.ts:45-48). A aba Recovered filtra fallback, o que bate. O failed_only saiu, e nenhum chamador usava.
  • AgentReliabilityRow, ProviderReliabilityRow e ModelReliabilityRow batem com getPerAgentStats, getPerProviderStats e getPerModelStats. O autofixed é enviado e ignorado.
  • ReliabilityKpiCards:
    • Recovered% = fallback_saves / total_requests, com guarda para total 0. Failed = errors_remaining, que no backend é t.error.
    • O grid de 4 colunas é o .overview-stats padrão. Isso também vale para ConnectionDetail, que agora tem 4 cards.
    • O deep link trigger=fallback é aceito por messages-query.dto.ts.
  • Tracking: a dependência ficou registrada na Remover Autofix/Phoenix do backend #16 (comentário de 2026-10-11, "Bloqueia o dashboard se for ignorado"). Isso é suficiente.

3. Chamadas de saída do navegador

Fiz o build do SHA localmente (vite build) e varri o bundle. Os resultados:

  • wingman, blue.manifest, /api/v1/version, telemetry.manifest, discovery/status|complete, Sentry, PostHog, gtag e Peacock: nenhuma ocorrência. autofix aparece só nos cinco caminhos overview/autofix-*.
  • Os fetch/EventSource do src vão todos para caminhos relativos da própria instância (/api/...).
  • Os hosts externos que sobram no bundle são href de links (consoles de provedores e manifest.build em Terms/Docs/AuthLayout, que já existiam e são da limpeza de rebrand) e placeholders. Nada é buscado sem clique.
  • As og:/twitter: meta em index.html não são requisitadas pelo navegador.
  • O @codecov/vite-plugin é de build e já tem ticket na CI completa com os nomes novos #20.

4. Renomeações

selfheal→recovery (MessageColumnKey, data-col), ChartTab selfheal→recovered, selfHealedCount→recoveredCount e AutofixTimeseries→StatusTimeseries são todos identificadores internos do frontend. Nenhum é persistido em localStorage ou na URL, e nenhum é nome de campo da API. Os campos da resposta continuam com os nomes do backend. O tsc --noEmit passa.

5. MessageDetails: layout triplo e onOpenMessage

  • Com o Autofix fora, hasTrigger = isFallbackTrigger = !!fallback_from_model && !isFallbackError e hasNextAction = isFallbackError (MessageDetails.tsx:106-111). Os dois são mutuamente exclusivos, então hasTrigger && hasNextAction nunca é verdadeiro. Na base, o fallback já era exclusivo: o layout triplo só ficava alcançável com isAutofixRetry/isAutofixOriginal. A remoção é segura, e a tentativa do meio de uma cadeia de fallback renderiza igual à base.
  • onOpenMessage só servia ao link para o irmão do Autofix (autofix_sibling). Não sobrou nenhuma referência em src nem em RequestDrawer/MessageLog.
  • As classes renomeadas (error-recovery-row*, recovery-card*) estão todas definidas em styles/data.css.

6. Testes

  • Os testes removidos (Discovery, discovery service, version-api, WingmanDevTools, wingman-dev-proxy, NotificationBell, SettingsAutofixSection, MessageDetails.autofix, MessageTable.onOpenMessage, routing-extra autofix) cobriam só comportamento removido.
  • O comportamento novo tem testes:
    • a chave legada não redireciona;
    • Setup vai para / ou /login;
    • Register e o login social vão para /welcome;
    • VersionIndicator não faz fetch e não tem link;
    • AddAgentModal não envia autofix_enabled;
    • o deep link antigo ?trigger=autofix,fallback vira fallback;
    • os caminhos RELIABILITY_PATHS;
    • os cards e gráficos em AnalyticsChartSurfaces e Overview.
  • Rodei localmente neste SHA:
    • tsc --noEmit: passa.
    • vite build: passa.
    • eslint src/: 0 erros e 49 warnings, os mesmos que o PR reporta.
    • TZ=UTC vitest run: 4104 testes passam. A única suíte com erro foi brand-assets.test.ts, por um artefato do meu checkout parcial, que não incluiu .github/assets/. Somando os 24 testes dela, o total bate com os 4128 que o PR reporta.

Achados

Blockers: nenhum.

Should-fix

  1. Default do Autofix entre esta PR e a Remover Autofix/Phoenix do backend #16, e a afirmação no guia. Arquivos: packages/frontend/src/components/AddAgentModal.tsx:80-85, packages/frontend/src/pages/Welcome.tsx:288-293 e docs/dev/manifest-guide.md:772. A causa está no backend: routing/autofix/autofix.service.ts:144 e common/utils/detect-self-hosted.ts:16-26.
    • Cenário: o backend roda fora de contêiner e sem MANIFEST_MODE (npm start em bare-metal, ou dev seguindo a linha 29 do guia, que ainda diz "default: cloud"). Aí isSelfHosted() retorna false e um agente criado sem autofix_enabled (NULL) resolve para ON. Requisições que falham com 4xx reparável passam a ser enviadas a https://autofix.manifest.build, e o dashboard já não tem nenhum controle para desligar isso, porque SettingsAutofixSection saiu. Agentes antigos com autofix_enabled=true também ficam sem desligamento pela UI.
    • Por que não é blocker: não é destino novo. Na base, a UI mandava true explícito por padrão, então o PR reduz a exposição: Docker, Podman e k8s ficam OFF.
    • Problema adicional: o guia afirma que "the self-hosted default (off) applies", o que é falso nesse cenário.
    • Correção sugerida: uma destas duas opções.

Nits

  1. Série healed residual. ReliabilityChart.tsx:34,56-62: se o backend devolver a chave healed (dados legados, ou o cenário do should-fix 1), a legenda mostra "Healed" em cinza #888. Além disso, o card Success rate inclui healed (autofix-stats.service.ts:487), enquanto Recovered não inclui. Isso some com a Remover Autofix/Phoenix do backend #16. Se quiser fechar antes, basta descartar healed em orderTimeseries.
  2. Deep link antigo ?trigger=autofix sozinho. MessageLog.tsx:247-252: cai em "All attempts" e deixa o parâmetro velho na URL. É inofensivo.
  3. Changeset. Não tem, e há mudança visível ao usuário. Isso segue o precedente da refactor(frontend): remove the SaaS layer from the dashboard #51 (as entradas de changelog ficam para a 0.1.0 da Release, Changesets 0.1.0 e publicação no Docker Hub #31), mas o corpo do PR não registra a justificativa. Sugiro adicionar uma linha dizendo isso.
  4. Teste de /cli/auth. Não há teste específico para /cli/auth sem sessão. Hoje o caso é coberto genericamente por "preserves the protected path and query". Um teste de /cli/auth?code=… → /login?redirect=… documentaria o contrato da CLI.

Gates

  • CI: o ci.yml só dispara em pull_request para main, então não roda neste PR empilhado (o único check é dependency-review). O gate de CI fica para quando o PR for retargetado para main depois do merge da refactor(frontend): remove the SaaS layer from the dashboard #51. Até lá valem as execuções locais reportadas no PR, que reproduzi acima.
  • qa-docker: pendente. É obrigatório, porque o PR muda comportamento de runtime: Setup, Register, Overview, Messages, Settings e criação de harness, com HAR mostrando só o host da instância. O próprio PR condiciona a saída de draft a ele.
  • Validade: este veredito vale só para o SHA abaixo. Qualquer commit novo, inclusive um rebase depois do merge da refactor(frontend): remove the SaaS layer from the dashboard #51, pede nova revisão.
  • Should-fixes: precisam estar corrigidos no PR ou rastreados em issue com link no PR antes do merge.

Head revisado: 071ae49
Veredito: risco alto (aguardar mantenedor)

Review follow-up for #57.

- A harness stored without autofix_enabled inherits the backend default.
  The backend turns Autofix ON when it detects no container (bare-metal
  npm start, dev runs), and the dashboard no longer has a switch to turn
  it off. Until #16 removes Autofix from the backend:
  - createAgent always sends autofix_enabled: false, which covers the
    Connect Harness modal, Welcome and ConnectProvider;
  - duplicateAgent follows the copy with PATCH
    /routing/:agent/autofix {enabled:false}, because the duplicate
    endpoint has no field for it.
  Both live in one commented block in services/api/agents.ts, with
  tests on the request payloads.
- The request-status timeseries client folds a legacy `healed` key into
  `success`. The backend's success rate already counts those requests
  as successes, so the bars still sum to the request total, Success +
  Recovered + Error matches the Success rate card, and Recovered stays
  fallback-only, like its KPI.
- The Requests page rewrites a stale ?trigger= value (for example
  ?trigger=autofix) on load to the filter it actually applies.
- Add AuthGuard tests for /cli/auth, with and without a session.
- Correct the dev guide: the self-hosted default is OFF only in a
  container or with MANIFEST_MODE=selfhosted. Document
  AUTOFIX_GLOBAL_ENABLED=false as the operator workaround for existing
  harnesses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao

Copy link
Copy Markdown
Owner Author

Re-revisão: PR #57 (head dd0ad62)

Escopo:

  • Delta: git diff 071ae49..dd0ad62b2d880134728583495827f2eccba3c516, um commit (fix(frontend): opt new harnesses out of legacy Autofix until #16) e 9 arquivos.
  • Sanidade: origin/feat/12-remove-saas-dashboard...dd0ad62, com o merge-base ainda em 5f0c407. Fora o delta, o diff é idêntico ao revisado em 071ae49.

Li o código ao redor nesse SHA, inclusive os handlers do backend que o delta chama.

Não há blocker. O should-fix e os nits da revisão anterior foram tratados. O veredito continua risco alto pelo mesmo critério: o PR muda os guards de sessão e os redirects pós-autenticação (AuthGuard, GuestGuard, Setup, Register). Pela ADR 0002, o merge fica com o mantenedor.

Delta verificado

1. Opt-out do Autofix legado (services/api/agents.ts)

  • createAgent: sempre envia autofix_enabled: false. Os três chamadores do frontend (AddAgentModal, Welcome e ConnectProvider) passam por ele, e não há outro POST /agents em src. No backend, agents.controller.ts:99 só grava o consentimento quando === true, então false não grava nada.
  • PATCH depois do duplicate: routing/tier.controller.ts:180-211 grava autofix_enabled=false resolvendo o agente por ctx.tenantId, então não há acesso entre tenants. Com enabled: false o handler não grava consentimento (o recordAutofixConsent só roda em body.enabled). Ele apenas invalida o resolveAgentService e a chave ${tenant}:/api/v1/autofix/status, o que é inofensivo.
  • Cache do Autofix: o AutofixService não guarda cache persistente da flag por agente. É só single-flight (autofix.service.ts:515-560), então a próxima requisição já lê false.
  • Corrida com o redirect: não há. DuplicateAgentModal.tsx:73 aguarda duplicateAgent, que só retorna depois do PATCH, e só então navega.
  • Erro: se o PATCH falhar, o fetchMutate já mostrou um toast.error e lançou. O catch engole o erro e a cópia é devolvida, e há teste para isso. Veja o nit 1.
  • routingPath(name, 'autofix'): encoda o nome e gera /routing/<slug>/autofix, coberto por teste.

2. Dobra de healed em success (analytics.ts, foldLegacyRecoveredKey)

  • Consistência com o backend: a dobra bate com autofix-stats.service.ts:487 (successes = success + healed + fallback). Com isso, Success + Recovered (fallback) + Error na série casa com o card Success rate. Recovered continua só fallback, igual ao KPI e a fallback_saves.
  • Algoritmo: o índice da chave legada é removido. Se não existir success, ele é criado na frente, e os demais contadores são remapeados pelo índice original. Cobre success ausente, ordem arbitrária (o backend coloca healed por último, porque DISPOSITION_ORDER não conhece essa chave) e o caso sem healed, que devolve a mesma referência. Tudo isso tem teste.
  • Alcance: a dobra só se aplica ao getRequestStatusTimeseries. A série de tentativas por conexão (attempt-stats.service.ts) só tem success e error, então não precisa dela.

3. Reescrita do ?trigger= (MessageLog.tsx:257-266)

  • Sem loop. Roda uma única vez em onMount. Só chama setSearchParams quando o valor bruto difere do aplicado, e usa replace: true, que não cria entrada no histórico.
  • O triggerFilter não é rederivado da URL depois do mount. Nenhum on(() => searchParams.…) observa trigger; os que existem observam agent, model, range e status.
  • O valor fallback não é reescrito. autofix vira undefined e não vai para a API, e autofix,fallback vira fallback. Os três casos têm teste.

4. Teste do /cli/auth

Os dois casos estão cobertos: sem sessão, redireciona para /login?redirect=<path+query PKCE> sem renderizar os filhos; com sessão, renderiza sem navegar.

5. Guia e corpo do PR

Execuções locais neste SHA

Comando Resultado
tsc --noEmit passa
eslint src/ 0 erros, 49 warnings (os mesmos de antes)
vite build passa
TZ=UTC vitest run 200 arquivos, 4137 testes passam

No bundle:

  • autofix aparece só nos cinco caminhos overview/autofix-* e no corpo autofix_enabled do opt-out.
  • Não há wingman, /api/v1/version, telemetry, discovery, Sentry nem PostHog.
  • O conjunto de hosts externos é idêntico ao do bundle de 071ae49: só links clicáveis.

Achados

Blockers: nenhum.

Should-fix: nenhum aberto.

Nits

  1. Falha do PATCH de opt-out é pouco clara. Em services/api/agents.ts (duplicateAgent, catch vazio), se o PATCH falhar, o usuário vê o toast.error genérico do backend e, logo depois, o toast "Duplicated…". A cópia fica com a flag NULL, ou seja, ON fora de contêiner, sem aviso específico. A janela é pequena e o contorno documentado (AUTOFIX_GLOBAL_ENABLED=false) cobre o caso. Se quiser, troque por um toast explícito ("Copy created, but legacy Autofix could not be turned off"). Não precisa de issue: a Remover Autofix/Phoenix do backend #16 apaga o bloco.
  2. Mesmo efeito para chamadores fora do dashboard. Agentes criados pela API, MCP ou CLI sem a flag continuam herdando o default. Isso é backend, está documentado no guia e é escopo da Remover Autofix/Phoenix do backend #16.

Gates

  • CI: o ci.yml só dispara em PRs para main, então não roda neste PR empilhado. O gate de CI é checado quando o PR for retargetado para main depois do merge da refactor(frontend): remove the SaaS layer from the dashboard #51. Até lá valem as execuções locais acima e as do PR.
  • qa-docker: ainda pendente e obrigatório (mudança de runtime). Incluir no roteiro:
    • a criação e a duplicação de harness, com o autofix_enabled resultante false;
    • o HAR mostrando só o host da instância.
  • Validade: o veredito vale só para o SHA abaixo. Qualquer commit novo, inclusive um rebase depois do merge da refactor(frontend): remove the SaaS layer from the dashboard #51, pede nova revisão.

Head revisado: dd0ad62
Veredito: risco alto (aguardar mantenedor)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao
patrickbrandao deleted the branch main October 11, 2026 16:58
Base automatically changed from feat/12-remove-saas-dashboard to main October 11, 2026 16:58
patrickbrandao and others added 2 commits October 11, 2026 13:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now holds the squash of #51, whose tree equals ce9fd33, which this
branch already merged, so the result keeps this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao

Copy link
Copy Markdown
Owner Author

Re-revisão: PR #57 depois do merge do #51 (head 17f7d5b)

Antes revisado: dd0ad62. Agora: 17f7d5bad11ff70e062e6004f8ae41adb7359eff, a ponta de feat/13-remove-phonehome-autofix-ui.

Atenção: o PR está fechado e o GitHub não vê este head

  • A timeline mostra três eventos às 16:58:36-37Z: base_ref_deleted, depois automatic_base_change_succeeded e depois closed. O PR foi fechado logo depois do retarget para a main.

  • Os pushes de bf0709e/17f7d5b vieram depois. Por isso o gh pr view 57 --json headRefOid e o refs/pull/57/head continuam em dd0ad62, enquanto a branch está em 17f7d5b, um fast-forward de dd0ad62.

  • Não há CI em 17f7d5b. O único check-run no SHA é o .github/dependabot.yml; o ci.yml só dispara em pull_request/push para a main.

  • Ação do Principal:

    • Reabrir o PR (gh pr reopen 57) e confirmar que headRefOid passa a ser 17f7d5b.
    • Esperar a CI completa nesse SHA.
    • Se o GitHub recusar a reabertura, abrir um PR novo da mesma branch.

    Este veredito vale só para 17f7d5b. Qualquer outro head pede nova revisão.

Topologia e equivalência

  • git diff ce9fd33 4d861c6 é vazio, então o merge -s ours de origin/main é válido. A árvore de 17f7d5b é idêntica à de bf0709e (o merge de ce9fd33).
  • git merge-base origin/main 17f7d5b = 4d861c6.
  • git diff origin/main...17f7d5b contra git diff 5f0c407...dd0ad62: os patches de todos os 98 arquivos são idênticos, ignorando as linhas index e os offsets dos hunks. Não sobra diferença.
  • Merge de ce9fd33 sem conflito, como relatado. Nenhum dos 20 arquivos que o PR apaga mudou entre 5f0c407 e ce9fd33. O único arquivo que a main (desde 5f0c407) e o PR tocam juntos é packages/frontend/src/index.tsx. Lá a main adicionou o import e a chamada de cleanUpStoredHeaders() (Playground: headers do adaptador sempre vencem e pendências do #104 #105/fix(frontend): warn on invalid Playground headers and stop storing values #106), e o PR remove a rota /discovery. No head, os dois coexistem.

Código novo da main contra o que o PR remove

Busquei no head os exports dos arquivos deletados (isRemoteWingman, resolveWingmanDrawerUrl, wingmanDevProxy, getVersionInfo, VersionInfo, completeDiscovery, isDiscoveryRequired, markDiscovery* e outros), os caminhos deletados e as rotas /discovery e /api/v1/version.

Execuções locais neste head

Comando Resultado
npm ci passa
build do shared passa
tsc --noEmit do frontend passa
eslint src/ 0 erros, 49 warnings (as mesmas de antes)
vite build passa
TZ=UTC vitest run 201 arquivos, 4180 testes passam
shared jest 23 suítes, 539 testes passam

Bundle: não aparecem wingman, /api/v1/version, PostHog nem Sentry. Os hosts externos são só links clicáveis, como na revisão anterior.

Blockers

Nenhum no código. O gate de CI ainda não foi cumprido; ver acima.

Should-fix

Nenhum.

Nits

  1. packages/backend/src/cors-csp-config.ts:159: o comentário aponta para packages/frontend/wingman-dev-proxy.ts, que não existe mais. Atualize na limpeza do backend (Remover Autofix/Phoenix do backend #16).
  2. packages/backend/src/notifications/emails/doctor-release.tsx:37: o template morto referencia o asset apagado. Remova junto com o Autofix do backend (Remover Autofix/Phoenix do backend #16).
  3. O nit 1 da revisão anterior (o toast do PATCH de opt-out no duplicateAgent) continua opcional.

Risco alto

O critério de antes continua valendo. O PR muda os guards de sessão e os redirects pós-autenticação (components/AuthGuard.tsx, components/GuestGuard.tsx, pages/Setup.tsx, pages/Register.tsx). Isso entra em "muda auth: sessões". O merge é decisão do mantenedor (ADR 0002).

Gates

Head revisado: 17f7d5b
Veredito: risco alto (aguardar mantenedor)

@patrickbrandao

Copy link
Copy Markdown
Owner Author

qa-docker: PASS

Tested SHA: 17f7d5bad11ff70e062e6004f8ae41adb7359eff (head of feat/13-remove-phonehome-autofix-ui and of the PR, confirmed with gh pr view and git ls-remote; base is current main 4d861c6). Image thinker:qa-8f3b71 built from docker/Dockerfile on arm64 in 24 s (warm cache), 466 MB. For the egress comparison I also built main 4d861c6 as thinker:qa-8f3b71-main. Stacks: isolated Compose projects (thinker-qa-8f3b71, thinker-qa-cccc9f, thinker-qa-baa846), volumes renamed through an override (name: !reset null), resolved names carried the run id, labels checked against config --volumes before every down -v, nothing left behind. Postgres 16.15 (postgres:16-alpine@sha256:721873c3...). Base images: node:24-alpine (build), node:24-slim (prod deps), gcr.io/distroless/nodejs24-debian13:nonroot (runtime), all by digest.

Step Result
Build pass (24 s)
Stack up on a fresh volume, /api/v1/health pass (healthy in about 8 s, migrations clean)
First-admin setup in Chromium (/setup, lands on /overview, no discovery step), cookie clear, /overview redirects to /login?redirect=..., sign-in at /login, signed-in /setup and /register redirect to /overview pass
Signed-in dashboard in headless Chromium: 31 URLs (overview, requests, harnesses, playground, providers, integrations, account, all harness tabs, legacy /agents/:name/overview redirect, /connect-provider, /cli/auth) plus the request drawer pass: 273 API calls, all 2xx, zero page errors, zero console errors/warnings, zero failed requests, zero requests to any non-app origin
/discovery, /upgrade, /free-models render "Page not found" (removed routes)
Harness create and duplicate through the UI pass: POST /agents sends autofix_enabled:false, duplicate is followed by PATCH /routing/<copy>/autofix {enabled:false}; DB shows autofix_enabled = f for both
?trigger=autofix is dropped (/messages), ?trigger=fallback is kept, autofix,fallback becomes fallback pass
Playground in the UI with a custom header X-QA-Trace: ui-trace-789, two columns through the host mock pass: both runs show mock reply, the mock received the header in both. Via API: events delta,delta,done; Authorization and Host overrides are not forwarded (mock saw the stored key and the real host)
Gateway through the host mock: /v1/models, chat completions (JSON, SSE), /v1/messages (Bearer, x-api-key, SSE), /v1/responses (JSON, SSE), no key gives 401; 60 more mixed requests pass, all 200
Built bundle grep (dist/assets, 173 files vs 170 on main) pass: no version-check/updateCheck//api/v1/version/releases/latest/api.github.com, no Sentry, PostHog, Plausible, Mixpanel, Stripe, Phoenix or Wingman. Main has Phoenix, Wingman and /api/v1/version hits; VersionIndicator is now a static build-time badge. gtag and amplitude hits are SVG/DOMPurify attribute lists. Autofix left only as the legacy backend endpoint names (/overview/autofix-stats, -timeseries, -per-*), the autofix_enabled:false opt-out and the PATCH /routing/:agent/autofix call
Egress, app container, default env (telemetry and update check not disabled), boot plus about 75 s idle plus a Chromium session, tcpdump sidecar in the app netns pass: public destinations in the PR are a strict subset of main (details below)
Contracts contracts/gateway against the container pass, 6/6
Backend e2e: TZ=UTC npm run test:e2e:docker --workspace=packages/backend (shared built first) pass, 64 suites, 554 tests, 115 s (127 s with container startup)
Teardown (down -v with -p, label check first) pass, for all three projects

Egress comparison (PR vs main, same procedure)

Outbound TCP SYNs from the app container to public addresses:

Destination main PR
104.26.8.108 (models.dev), 104.18.2.115 (openrouter.ai), 185.199.111.133 (raw.githubusercontent.com), 216.150.1.1, all within 1 s of boot yes yes
69.46.46.119 (*.up.railway.app, the anonymous usage telemetry collector), 0.3 s after boot yes yes
4.228.31.149 (api.github.com) 4 s later, triggered by the dashboard calling GET /api/v1/version (14 calls in the session) yes no

Nothing new was contacted during the idle period in either run. In the browser, the PR dashboard issues no /api/v1/version, /api/v1/autofix/status or /api/v1/routing/<agent>/autofix calls (all three are present on main), and no request left the app origin. The only remaining non-provider egress is backend-side and unchanged from main.

Observations (not blockers, nothing flaky, nothing needed a retry):

  • The backend's anonymous telemetry to *.up.railway.app is still on by default (MANIFEST_TELEMETRY_DISABLED=0 in the compose file) and the VersionModule/GithubModule endpoints still exist. Both are outside this PR (frontend) but contradict the no-phone-home rule; they should be covered by the backend cleanup.
  • The dashboard still calls backend endpoints named autofix-* for the reliability charts, as the PR description says.
  • On first boot with an empty database, Better Auth logs ERROR ... Database schema mismatch before migrations create its tables. Identical on main.

@patrickbrandao
patrickbrandao merged commit 3f448ff into main Oct 11, 2026
12 checks passed
@patrickbrandao
patrickbrandao deleted the feat/13-remove-phonehome-autofix-ui branch October 11, 2026 17:22
patrickbrandao added a commit that referenced this pull request Oct 11, 2026
Brings in 498de23 (main plus #60) so #16 can merge into main after #60.
Conflicts came from #57's interim dashboard handling of the legacy Autofix
backend overlapping with this branch's backend removal:

- frontend analytics client: RELIABILITY_PATHS points at the neutral
  /overview/reliability/* routes. foldLegacyRecoveredKey is dropped: the
  backend computes the request disposition from success/fallback only and
  DropAutofixSchema removes requests.autofix_status, so no row can come back
  as "healed", and the dashboard ships with its own backend.
- frontend agents client: the auto-merged autofix_enabled opt-out on create
  and the PATCH /routing/:agent/autofix after duplicate are dropped (the field
  and the endpoint no longer exist; create rejects the field).
- shared: keep the playground-headers exports, drop the autofix-status ones.
- tests: keep this branch's versions (neutral paths, no opt-out, no fold).
- e2e specs from main (local providers, reserved local runner aliases): stop
  sending autofix_enabled on agent create.
- manifest guide: keep the "Autofix removed" section and neutral-path wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
patrickbrandao added a commit that referenced this pull request Oct 11, 2026
#66)

* refactor(frontend): remove the SaaS layer from the dashboard

Thinker is self-hosted only, so the inherited Manifest Cloud UI goes.
The frontend leaves first so no screen breaks when #14/#15 drop the
matching backend endpoints.

- Billing/plans: delete PlanPicker, UpgradeSuccessModal,
  UsageLimitBanner, the /upgrade page, the Register ?step=plan picker,
  the plan stores, the plan-based range locks on the analytics pages,
  the plan-limit upgrade CTAs in the request views, the Account billing
  and usage-email sections, and the @better-auth/stripe client plugin
  and dependency.
- API Bot: delete the banner, the sidebar card, the dithered ground,
  their stylesheet and the SVG asset.
- GitHub stars: drop the header star button and its /api/v1/github call.
- Free Models: delete the page, its route, its API client and the assets
  only it used.
- Managed Gemini Free: drop the provider entry, icon alias, background
  /ensure provisioning call and the Calendly key URL.
- Cloud branches: checkIsSelfHosted() is gone and every screen behaves
  as self-hosted (Local providers always listed, local logs copy, CLI
  login pinned to this host, discovery after signup and setup). Drop the
  Manifest Cloud origin mapping, the cloud email card, the
  VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it.
- Help: drop the Manifest Calendly booking link.
- Remove the dead CSS for all of the above, update the tests and the
  inherited dev guide.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove billing, Stripe and plan limits

Billing, Stripe and the Free/Pro plans belong to the SaaS layer that
Thinker drops (docs/decisoes-iniciais.md section 3).

- Delete packages/backend/src/billing (PlanService, billing controller,
  billing emails, request quota window, subscription status) and drop
  BillingModule from the app, proxy and analytics modules.
- Proxy: remove the per-request plan gate (assertWithinRequestLimit) and
  the PLAN_LIMIT_REQUESTS / M204 handling in the controller and the
  exception filter. The proxy no longer answers 402 on its own; a 402
  follows the generic error path.
- Auth: remove the @better-auth/stripe plugin; drop the stripe and
  @better-auth/stripe dependencies from the backend.
- Recording retention: keep only the global path (365-day default or
  REQUEST_RECORDING_RETENTION_DAYS); per-plan retention is gone.
- Tenant entity: drop limit_overrides and billing_email_preferences;
  delete the TenantRequestUsage entity.
- Shared: delete plan-limits and its exports.
- Remove the STRIPE_* / PLAN_LIMIT_* / PLAN_REQUEST_QUOTA_RESET_AT
  variables from .env.example.
- New migration DropBillingSchema1803200000000 drops the quota trigger
  and function, requests.quota_counted, tenant_request_usage,
  billing_email_logs and the two tenant columns. The old
  AddTenantRequestUsage migration keeps only its self-hosted path
  (a no-op up) so it no longer imports deleted billing code.

/api/v1/billing/* no longer exists, and the STRIPE_* and PLAN_LIMIT_*
variables are ignored if set.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(frontend): remove phone-home and Autofix from the dashboard

Thinker makes no outbound calls of its own, and Autofix depended on
Manifest's hosted Phoenix service. The dashboard drops both before the
backend modules go in #16 and #17.

- Update check: VersionIndicator now only shows the build-time
  __MANIFEST_VERSION__ badge. It makes no request, shows no "new version"
  link to the manifest.build changelog and has no dismiss state.
  services/api/version.ts and its test are gone.
- Discovery: delete the post-signup questionnaire (page, service, styles,
  route, tests) and the pending-discovery redirects in AuthGuard and
  GuestGuard. First-admin setup now lands on / (Overview), signup on
  /welcome, and social signup also goes to /welcome. A leftover
  manifest_discovery_pending_* key from an older build no longer
  redirects anyone.
- Wingman: delete the dev drawer, wingman-dev-proxy.ts, the
  VITE_WINGMAN_* / __WINGMAN_URL__ / __DEV_MODE__ build constants, the
  App and Welcome mounts, the drawer CSS and the CONTRIBUTING section.
- Autofix: delete AutofixKpiCards, SettingsAutofixSection,
  NotificationBell (it only nagged about inactive Autofix), the Autofix
  switch and Manifest Terms/Privacy line on harness creation (modal and
  Welcome), the autofix badges, trigger filter, Phoenix panels, sibling
  links and status labels in the Requests views, the "Autofixed attempts"
  connection card, the Doctor tooltip variant, the healed chart series,
  the autofix fields in services/api/*, the two public PNGs, and the
  dead CSS.
- The generic reliability metrics that the backend still serves under
  the legacy /overview/autofix-* paths (success rate, fallback
  recoveries, request status over time, per harness/provider/model
  reliability) stay. They are now read through neutral clients
  (getReliabilityStats, getRequestStatusTimeseries, RELIABILITY_PATHS)
  and shown by ReliabilityKpiCards. "Self-healed" becomes "Recovered" and
  counts fallback recoveries only.
- Update the tests, the design-system registry and the inherited dev
  guide.

Closes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): opt new harnesses out of legacy Autofix until #16

Review follow-up for #57.

- A harness stored without autofix_enabled inherits the backend default.
  The backend turns Autofix ON when it detects no container (bare-metal
  npm start, dev runs), and the dashboard no longer has a switch to turn
  it off. Until #16 removes Autofix from the backend:
  - createAgent always sends autofix_enabled: false, which covers the
    Connect Harness modal, Welcome and ConnectProvider;
  - duplicateAgent follows the copy with PATCH
    /routing/:agent/autofix {enabled:false}, because the duplicate
    endpoint has no field for it.
  Both live in one commented block in services/api/agents.ts, with
  tests on the request payloads.
- The request-status timeseries client folds a legacy `healed` key into
  `success`. The backend's success rate already counts those requests
  as successes, so the bars still sum to the request total, Success +
  Recovered + Error matches the Success rate card, and Recovered stays
  fallback-only, like its KPI.
- The Requests page rewrites a stale ?trigger= value (for example
  ?trigger=autofix) on load to the filter it actually applies.
- Add AuthGuard tests for /cli/auth, with and without a session.
- Correct the dev guide: the self-hosted default is OFF only in a
  container or with MANIFEST_MODE=selfhosted. Document
  AUTOFIX_GLOBAL_ENABLED=false as the operator workaround for existing
  harnesses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove waitlist, CRM, Gemini Free, free models, GitHub stars and error pages

Remove the Manifest Cloud and marketing surfaces from the backend:

- waitlist/ (pivot and Autofix claims) and its open-CORS pivot route
  (applyPivotClaimCors, PIVOT_CLAIM_PATH, PIVOT_CLAIM_CLOUD_ORIGIN).
- crm-metrics/ and CRM_METRICS_SECRET.
- The managed Gemini Free provider: routing/managed-free-provider/,
  common/constants/managed-free-providers.ts, the CREDITS_* variables, and
  its entries in provider-endpoints, provider-client, provider.service,
  provider-model-fetcher and routing.module. No other provider's endpoint,
  prefix handling, fallback order or tier resolution changes.
- free-models/ (sync from mnfst/awesome-free-llm-apis), github/ (stars from
  api.github.com) and error-pages/ (public and Peacock push routes, seeders,
  discovery), MANIFEST_PUBLIC_STATS and ERROR_PAGE_PUSH_SECRET.
- gemini-free and the managedFree flag from manifest-shared, and the
  regenerated CLI provider catalog.
- The plan_request_limit_exceeded error class left over from #14.

DropSaasFeatureSchema1803300000000 sets a transaction-local lock_timeout,
drops waitlist_claims, public_error_pages and the cloud-only
IDX_requests_autofix_healed index, and deletes every gemini-free connection
with its agent grants. Request history keeps its provider label, and routes
that still name gemini-free resolve as unavailable and fall through to their
fallbacks, as after a disconnect. down() restores the two tables as the
self-hosted path created them.

Closes #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove Autofix and the Phoenix healer from the backend

Autofix sent failing requests to Manifest's hosted healer
(autofix.manifest.build) and pinged it at every production boot. Thinker
makes no such call (#16). The local self-heal of v0.6 will build on the
fallback chain on its own terms.

- Delete routing/autofix (AutofixService, healing clients, health probe,
  observation reporter, Phoenix types and vendored contract) and the
  contract:refresh script with its ajv/js-yaml devDependencies.
- Proxy: drop the heal-once branch from the primary and fallback hops and
  the wire-body retry hook from ForwardResult. A failed request now goes
  straight to shouldTriggerFallback, exactly as for an agent that never
  opted in. The primary is always attempt 1, the x-manifest-provider /
  x-manifest-auth-type replay rewrite is gone, and the recorder writes no
  repair columns or auto_fixed rows.
- Analytics: move the generic reliability metrics to
  /api/v1/overview/reliability/{stats,timeseries,per-agent,per-provider,
  per-model} without the Autofix fields; drop the healed disposition, the
  auto_fixed counter, the autofix trigger filter and the connection
  breakdown's Autofix attempts.
- Remove /autofix/status, /autofix/enable-all, the per-harness
  /routing/:agent/autofix endpoints, the autofix_enabled create field, the
  MCP Autofix tools and the CLI `routing autofix` command and `--autofix`.
- Schema: DropAutofixSchema1803400000000 drops the autofix columns, check
  constraint and indexes (lock_timeout, IF EXISTS, full revert).
  1802200000000 now runs its self-hosted no-op path everywhere.
- Shared: drop deriveAutofixStatus, AUTOFIX_STATUSES and auto_fixed.
- Delete the unused doctor-release email.
- Frontend: call the neutral reliability paths and drop the temporary
  autofix_enabled opt-out, the duplicate PATCH and the healed fold.
- Docs: move the PoC spec to docs/referencia/, remove AUTOFIX_* from the
  guide, .env.example and the Docker README.

BREAKING CHANGE: the Autofix API, analytics paths, MCP tools and CLI
command are removed; the reliability analytics moved to
/api/v1/overview/reliability/*.

Closes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the billing drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the SaaS drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the Autofix drop e2e through the database guard

The DropAutofixSchema e2e spec opens its own DataSource with dropSchema,
so it now takes its URL from resolveE2eDatabaseUrl() like the other
migration specs instead of falling back to a default localhost database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
patrickbrandao added a commit that referenced this pull request Oct 11, 2026
* refactor(frontend): remove the SaaS layer from the dashboard

Thinker is self-hosted only, so the inherited Manifest Cloud UI goes.
The frontend leaves first so no screen breaks when #14/#15 drop the
matching backend endpoints.

- Billing/plans: delete PlanPicker, UpgradeSuccessModal,
  UsageLimitBanner, the /upgrade page, the Register ?step=plan picker,
  the plan stores, the plan-based range locks on the analytics pages,
  the plan-limit upgrade CTAs in the request views, the Account billing
  and usage-email sections, and the @better-auth/stripe client plugin
  and dependency.
- API Bot: delete the banner, the sidebar card, the dithered ground,
  their stylesheet and the SVG asset.
- GitHub stars: drop the header star button and its /api/v1/github call.
- Free Models: delete the page, its route, its API client and the assets
  only it used.
- Managed Gemini Free: drop the provider entry, icon alias, background
  /ensure provisioning call and the Calendly key URL.
- Cloud branches: checkIsSelfHosted() is gone and every screen behaves
  as self-hosted (Local providers always listed, local logs copy, CLI
  login pinned to this host, discovery after signup and setup). Drop the
  Manifest Cloud origin mapping, the cloud email card, the
  VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it.
- Help: drop the Manifest Calendly booking link.
- Remove the dead CSS for all of the above, update the tests and the
  inherited dev guide.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove billing, Stripe and plan limits

Billing, Stripe and the Free/Pro plans belong to the SaaS layer that
Thinker drops (docs/decisoes-iniciais.md section 3).

- Delete packages/backend/src/billing (PlanService, billing controller,
  billing emails, request quota window, subscription status) and drop
  BillingModule from the app, proxy and analytics modules.
- Proxy: remove the per-request plan gate (assertWithinRequestLimit) and
  the PLAN_LIMIT_REQUESTS / M204 handling in the controller and the
  exception filter. The proxy no longer answers 402 on its own; a 402
  follows the generic error path.
- Auth: remove the @better-auth/stripe plugin; drop the stripe and
  @better-auth/stripe dependencies from the backend.
- Recording retention: keep only the global path (365-day default or
  REQUEST_RECORDING_RETENTION_DAYS); per-plan retention is gone.
- Tenant entity: drop limit_overrides and billing_email_preferences;
  delete the TenantRequestUsage entity.
- Shared: delete plan-limits and its exports.
- Remove the STRIPE_* / PLAN_LIMIT_* / PLAN_REQUEST_QUOTA_RESET_AT
  variables from .env.example.
- New migration DropBillingSchema1803200000000 drops the quota trigger
  and function, requests.quota_counted, tenant_request_usage,
  billing_email_logs and the two tenant columns. The old
  AddTenantRequestUsage migration keeps only its self-hosted path
  (a no-op up) so it no longer imports deleted billing code.

/api/v1/billing/* no longer exists, and the STRIPE_* and PLAN_LIMIT_*
variables are ignored if set.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(frontend): remove phone-home and Autofix from the dashboard

Thinker makes no outbound calls of its own, and Autofix depended on
Manifest's hosted Phoenix service. The dashboard drops both before the
backend modules go in #16 and #17.

- Update check: VersionIndicator now only shows the build-time
  __MANIFEST_VERSION__ badge. It makes no request, shows no "new version"
  link to the manifest.build changelog and has no dismiss state.
  services/api/version.ts and its test are gone.
- Discovery: delete the post-signup questionnaire (page, service, styles,
  route, tests) and the pending-discovery redirects in AuthGuard and
  GuestGuard. First-admin setup now lands on / (Overview), signup on
  /welcome, and social signup also goes to /welcome. A leftover
  manifest_discovery_pending_* key from an older build no longer
  redirects anyone.
- Wingman: delete the dev drawer, wingman-dev-proxy.ts, the
  VITE_WINGMAN_* / __WINGMAN_URL__ / __DEV_MODE__ build constants, the
  App and Welcome mounts, the drawer CSS and the CONTRIBUTING section.
- Autofix: delete AutofixKpiCards, SettingsAutofixSection,
  NotificationBell (it only nagged about inactive Autofix), the Autofix
  switch and Manifest Terms/Privacy line on harness creation (modal and
  Welcome), the autofix badges, trigger filter, Phoenix panels, sibling
  links and status labels in the Requests views, the "Autofixed attempts"
  connection card, the Doctor tooltip variant, the healed chart series,
  the autofix fields in services/api/*, the two public PNGs, and the
  dead CSS.
- The generic reliability metrics that the backend still serves under
  the legacy /overview/autofix-* paths (success rate, fallback
  recoveries, request status over time, per harness/provider/model
  reliability) stay. They are now read through neutral clients
  (getReliabilityStats, getRequestStatusTimeseries, RELIABILITY_PATHS)
  and shown by ReliabilityKpiCards. "Self-healed" becomes "Recovered" and
  counts fallback recoveries only.
- Update the tests, the design-system registry and the inherited dev
  guide.

Closes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): opt new harnesses out of legacy Autofix until #16

Review follow-up for #57.

- A harness stored without autofix_enabled inherits the backend default.
  The backend turns Autofix ON when it detects no container (bare-metal
  npm start, dev runs), and the dashboard no longer has a switch to turn
  it off. Until #16 removes Autofix from the backend:
  - createAgent always sends autofix_enabled: false, which covers the
    Connect Harness modal, Welcome and ConnectProvider;
  - duplicateAgent follows the copy with PATCH
    /routing/:agent/autofix {enabled:false}, because the duplicate
    endpoint has no field for it.
  Both live in one commented block in services/api/agents.ts, with
  tests on the request payloads.
- The request-status timeseries client folds a legacy `healed` key into
  `success`. The backend's success rate already counts those requests
  as successes, so the bars still sum to the request total, Success +
  Recovered + Error matches the Success rate card, and Recovered stays
  fallback-only, like its KPI.
- The Requests page rewrites a stale ?trigger= value (for example
  ?trigger=autofix) on load to the filter it actually applies.
- Add AuthGuard tests for /cli/auth, with and without a session.
- Correct the dev guide: the self-hosted default is OFF only in a
  container or with MANIFEST_MODE=selfhosted. Document
  AUTOFIX_GLOBAL_ENABLED=false as the operator workaround for existing
  harnesses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove waitlist, CRM, Gemini Free, free models, GitHub stars and error pages

Remove the Manifest Cloud and marketing surfaces from the backend:

- waitlist/ (pivot and Autofix claims) and its open-CORS pivot route
  (applyPivotClaimCors, PIVOT_CLAIM_PATH, PIVOT_CLAIM_CLOUD_ORIGIN).
- crm-metrics/ and CRM_METRICS_SECRET.
- The managed Gemini Free provider: routing/managed-free-provider/,
  common/constants/managed-free-providers.ts, the CREDITS_* variables, and
  its entries in provider-endpoints, provider-client, provider.service,
  provider-model-fetcher and routing.module. No other provider's endpoint,
  prefix handling, fallback order or tier resolution changes.
- free-models/ (sync from mnfst/awesome-free-llm-apis), github/ (stars from
  api.github.com) and error-pages/ (public and Peacock push routes, seeders,
  discovery), MANIFEST_PUBLIC_STATS and ERROR_PAGE_PUSH_SECRET.
- gemini-free and the managedFree flag from manifest-shared, and the
  regenerated CLI provider catalog.
- The plan_request_limit_exceeded error class left over from #14.

DropSaasFeatureSchema1803300000000 sets a transaction-local lock_timeout,
drops waitlist_claims, public_error_pages and the cloud-only
IDX_requests_autofix_healed index, and deletes every gemini-free connection
with its agent grants. Request history keeps its provider label, and routes
that still name gemini-free resolve as unavailable and fall through to their
fallbacks, as after a disconnect. down() restores the two tables as the
self-hosted path created them.

Closes #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove Autofix and the Phoenix healer from the backend

Autofix sent failing requests to Manifest's hosted healer
(autofix.manifest.build) and pinged it at every production boot. Thinker
makes no such call (#16). The local self-heal of v0.6 will build on the
fallback chain on its own terms.

- Delete routing/autofix (AutofixService, healing clients, health probe,
  observation reporter, Phoenix types and vendored contract) and the
  contract:refresh script with its ajv/js-yaml devDependencies.
- Proxy: drop the heal-once branch from the primary and fallback hops and
  the wire-body retry hook from ForwardResult. A failed request now goes
  straight to shouldTriggerFallback, exactly as for an agent that never
  opted in. The primary is always attempt 1, the x-manifest-provider /
  x-manifest-auth-type replay rewrite is gone, and the recorder writes no
  repair columns or auto_fixed rows.
- Analytics: move the generic reliability metrics to
  /api/v1/overview/reliability/{stats,timeseries,per-agent,per-provider,
  per-model} without the Autofix fields; drop the healed disposition, the
  auto_fixed counter, the autofix trigger filter and the connection
  breakdown's Autofix attempts.
- Remove /autofix/status, /autofix/enable-all, the per-harness
  /routing/:agent/autofix endpoints, the autofix_enabled create field, the
  MCP Autofix tools and the CLI `routing autofix` command and `--autofix`.
- Schema: DropAutofixSchema1803400000000 drops the autofix columns, check
  constraint and indexes (lock_timeout, IF EXISTS, full revert).
  1802200000000 now runs its self-hosted no-op path everywhere.
- Shared: drop deriveAutofixStatus, AUTOFIX_STATUSES and auto_fixed.
- Delete the unused doctor-release email.
- Frontend: call the neutral reliability paths and drop the temporary
  autofix_enabled opt-out, the duplicate PATCH and the healed fold.
- Docs: move the PoC spec to docs/referencia/, remove AUTOFIX_* from the
  guide, .env.example and the Docker README.

BREAKING CHANGE: the Autofix API, analytics paths, MCP tools and CLI
command are removed; the reliability analytics moved to
/api/v1/overview/reliability/*.

Closes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove phone-home from the backend and the CLI

Thinker makes no outbound call of its own. Remove every upstream
phone-home from the backend and the CLI:

- usage telemetry (telemetry/, InstallIdService, PayloadBuilderService,
  the InstallMetadata entity) and the CLI usage spool
  (packages/cli/src/telemetry.ts);
- the daily update check (version/, GET /api/v1/version, the GitHub
  releases URL);
- the discovery forwarding to blue.manifest.build (discovery/,
  POST /api/v1/discovery/complete);
- Sentry (instrument.ts, sentry/, SentryModule/SentryGlobalFilter,
  the @sentry/nestjs dependency);
- the hosted Wingman origin in CORS and CSP frame-src; frame-src is
  'self' in every mode and WINGMAN_CORS_ORIGINS becomes
  THINKER_CORS_ORIGINS with the same meaning;
- the modelparams.dev fetch on boot and hourly cron; parameter specs
  come only from the bundled modelparams package snapshot.

DropInstallMetadata1803500000000 drops the install_metadata table
(lock_timeout, IF EXISTS, revert recreates it empty).

The removed variables leave docker-compose, the .env examples, the
Docker README, the deploy templates and the developer guide.

A new e2e guard fails if the backend, CLI or shared source names a
removed destination again, or if a production-mode boot of the real
server fetches or resolves anything outside the ADR 0003 catalogs.

BREAKING CHANGE: GET /api/v1/version and POST /api/v1/discovery/complete
are gone, the hosted Wingman origin is no longer allowed by CORS, and
WINGMAN_CORS_ORIGINS is renamed THINKER_CORS_ORIGINS.

Closes #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): default to a local gateway instead of Manifest Cloud

With no --url, MANIFEST_URL or stored login, the CLI targeted
https://app.manifest.build, so a MANIFEST_API_KEY set without a host
went to Manifest's cloud. DEFAULT_URL is now http://localhost:2099, the
default Thinker port, and the invalid-URL hint and README follow.

The backend's 404 hint for /chat/completions told callers to point
their baseURL at https://app.manifest.build/v1; it now refers to this
gateway.

The egress guard now fails on app.manifest.build or
gateway.manifest.build anywhere in the CLI or shared source (tests
included), on a new backend production file naming them, on a CLI
default that is not local, and on a boot request to either host. The
three remaining backend files (Cloud trusted origins, the dashboard
link fallback and the OG-tag placeholder) are pinned for #18, #22 and
#25 and may only shrink.

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the billing drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the SaaS drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the Autofix drop e2e through the database guard

The DropAutofixSchema e2e spec opens its own DataSource with dropSchema,
so it now takes its URL from resolveE2eDatabaseUrl() like the other
migration specs instead of falling back to a default localhost database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the install-metadata drop e2e through the database guard

The DropInstallMetadata migration spec and the no-phone-home egress spec
both open a dropSchema connection. Resolve their URL with
resolveE2eDatabaseUrl() from test/e2e-db-guard instead of falling back to
localhost:5432, so neither can wipe a database that is not an explicit,
disposable test database (#67, #69).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): escape host names fully in the egress spec patterns

CodeQL flagged js/incomplete-sanitization: only dots were escaped before
building a RegExp from the host constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
patrickbrandao added a commit that referenced this pull request Oct 11, 2026
* refactor(frontend): remove the SaaS layer from the dashboard

Thinker is self-hosted only, so the inherited Manifest Cloud UI goes.
The frontend leaves first so no screen breaks when #14/#15 drop the
matching backend endpoints.

- Billing/plans: delete PlanPicker, UpgradeSuccessModal,
  UsageLimitBanner, the /upgrade page, the Register ?step=plan picker,
  the plan stores, the plan-based range locks on the analytics pages,
  the plan-limit upgrade CTAs in the request views, the Account billing
  and usage-email sections, and the @better-auth/stripe client plugin
  and dependency.
- API Bot: delete the banner, the sidebar card, the dithered ground,
  their stylesheet and the SVG asset.
- GitHub stars: drop the header star button and its /api/v1/github call.
- Free Models: delete the page, its route, its API client and the assets
  only it used.
- Managed Gemini Free: drop the provider entry, icon alias, background
  /ensure provisioning call and the Calendly key URL.
- Cloud branches: checkIsSelfHosted() is gone and every screen behaves
  as self-hosted (Local providers always listed, local logs copy, CLI
  login pinned to this host, discovery after signup and setup). Drop the
  Manifest Cloud origin mapping, the cloud email card, the
  VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it.
- Help: drop the Manifest Calendly booking link.
- Remove the dead CSS for all of the above, update the tests and the
  inherited dev guide.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove billing, Stripe and plan limits

Billing, Stripe and the Free/Pro plans belong to the SaaS layer that
Thinker drops (docs/decisoes-iniciais.md section 3).

- Delete packages/backend/src/billing (PlanService, billing controller,
  billing emails, request quota window, subscription status) and drop
  BillingModule from the app, proxy and analytics modules.
- Proxy: remove the per-request plan gate (assertWithinRequestLimit) and
  the PLAN_LIMIT_REQUESTS / M204 handling in the controller and the
  exception filter. The proxy no longer answers 402 on its own; a 402
  follows the generic error path.
- Auth: remove the @better-auth/stripe plugin; drop the stripe and
  @better-auth/stripe dependencies from the backend.
- Recording retention: keep only the global path (365-day default or
  REQUEST_RECORDING_RETENTION_DAYS); per-plan retention is gone.
- Tenant entity: drop limit_overrides and billing_email_preferences;
  delete the TenantRequestUsage entity.
- Shared: delete plan-limits and its exports.
- Remove the STRIPE_* / PLAN_LIMIT_* / PLAN_REQUEST_QUOTA_RESET_AT
  variables from .env.example.
- New migration DropBillingSchema1803200000000 drops the quota trigger
  and function, requests.quota_counted, tenant_request_usage,
  billing_email_logs and the two tenant columns. The old
  AddTenantRequestUsage migration keeps only its self-hosted path
  (a no-op up) so it no longer imports deleted billing code.

/api/v1/billing/* no longer exists, and the STRIPE_* and PLAN_LIMIT_*
variables are ignored if set.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(frontend): remove phone-home and Autofix from the dashboard

Thinker makes no outbound calls of its own, and Autofix depended on
Manifest's hosted Phoenix service. The dashboard drops both before the
backend modules go in #16 and #17.

- Update check: VersionIndicator now only shows the build-time
  __MANIFEST_VERSION__ badge. It makes no request, shows no "new version"
  link to the manifest.build changelog and has no dismiss state.
  services/api/version.ts and its test are gone.
- Discovery: delete the post-signup questionnaire (page, service, styles,
  route, tests) and the pending-discovery redirects in AuthGuard and
  GuestGuard. First-admin setup now lands on / (Overview), signup on
  /welcome, and social signup also goes to /welcome. A leftover
  manifest_discovery_pending_* key from an older build no longer
  redirects anyone.
- Wingman: delete the dev drawer, wingman-dev-proxy.ts, the
  VITE_WINGMAN_* / __WINGMAN_URL__ / __DEV_MODE__ build constants, the
  App and Welcome mounts, the drawer CSS and the CONTRIBUTING section.
- Autofix: delete AutofixKpiCards, SettingsAutofixSection,
  NotificationBell (it only nagged about inactive Autofix), the Autofix
  switch and Manifest Terms/Privacy line on harness creation (modal and
  Welcome), the autofix badges, trigger filter, Phoenix panels, sibling
  links and status labels in the Requests views, the "Autofixed attempts"
  connection card, the Doctor tooltip variant, the healed chart series,
  the autofix fields in services/api/*, the two public PNGs, and the
  dead CSS.
- The generic reliability metrics that the backend still serves under
  the legacy /overview/autofix-* paths (success rate, fallback
  recoveries, request status over time, per harness/provider/model
  reliability) stay. They are now read through neutral clients
  (getReliabilityStats, getRequestStatusTimeseries, RELIABILITY_PATHS)
  and shown by ReliabilityKpiCards. "Self-healed" becomes "Recovered" and
  counts fallback recoveries only.
- Update the tests, the design-system registry and the inherited dev
  guide.

Closes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): opt new harnesses out of legacy Autofix until #16

Review follow-up for #57.

- A harness stored without autofix_enabled inherits the backend default.
  The backend turns Autofix ON when it detects no container (bare-metal
  npm start, dev runs), and the dashboard no longer has a switch to turn
  it off. Until #16 removes Autofix from the backend:
  - createAgent always sends autofix_enabled: false, which covers the
    Connect Harness modal, Welcome and ConnectProvider;
  - duplicateAgent follows the copy with PATCH
    /routing/:agent/autofix {enabled:false}, because the duplicate
    endpoint has no field for it.
  Both live in one commented block in services/api/agents.ts, with
  tests on the request payloads.
- The request-status timeseries client folds a legacy `healed` key into
  `success`. The backend's success rate already counts those requests
  as successes, so the bars still sum to the request total, Success +
  Recovered + Error matches the Success rate card, and Recovered stays
  fallback-only, like its KPI.
- The Requests page rewrites a stale ?trigger= value (for example
  ?trigger=autofix) on load to the filter it actually applies.
- Add AuthGuard tests for /cli/auth, with and without a session.
- Correct the dev guide: the self-hosted default is OFF only in a
  container or with MANIFEST_MODE=selfhosted. Document
  AUTOFIX_GLOBAL_ENABLED=false as the operator workaround for existing
  harnesses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove waitlist, CRM, Gemini Free, free models, GitHub stars and error pages

Remove the Manifest Cloud and marketing surfaces from the backend:

- waitlist/ (pivot and Autofix claims) and its open-CORS pivot route
  (applyPivotClaimCors, PIVOT_CLAIM_PATH, PIVOT_CLAIM_CLOUD_ORIGIN).
- crm-metrics/ and CRM_METRICS_SECRET.
- The managed Gemini Free provider: routing/managed-free-provider/,
  common/constants/managed-free-providers.ts, the CREDITS_* variables, and
  its entries in provider-endpoints, provider-client, provider.service,
  provider-model-fetcher and routing.module. No other provider's endpoint,
  prefix handling, fallback order or tier resolution changes.
- free-models/ (sync from mnfst/awesome-free-llm-apis), github/ (stars from
  api.github.com) and error-pages/ (public and Peacock push routes, seeders,
  discovery), MANIFEST_PUBLIC_STATS and ERROR_PAGE_PUSH_SECRET.
- gemini-free and the managedFree flag from manifest-shared, and the
  regenerated CLI provider catalog.
- The plan_request_limit_exceeded error class left over from #14.

DropSaasFeatureSchema1803300000000 sets a transaction-local lock_timeout,
drops waitlist_claims, public_error_pages and the cloud-only
IDX_requests_autofix_healed index, and deletes every gemini-free connection
with its agent grants. Request history keeps its provider label, and routes
that still name gemini-free resolve as unavailable and fall through to their
fallbacks, as after a disconnect. down() restores the two tables as the
self-hosted path created them.

Closes #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove Autofix and the Phoenix healer from the backend

Autofix sent failing requests to Manifest's hosted healer
(autofix.manifest.build) and pinged it at every production boot. Thinker
makes no such call (#16). The local self-heal of v0.6 will build on the
fallback chain on its own terms.

- Delete routing/autofix (AutofixService, healing clients, health probe,
  observation reporter, Phoenix types and vendored contract) and the
  contract:refresh script with its ajv/js-yaml devDependencies.
- Proxy: drop the heal-once branch from the primary and fallback hops and
  the wire-body retry hook from ForwardResult. A failed request now goes
  straight to shouldTriggerFallback, exactly as for an agent that never
  opted in. The primary is always attempt 1, the x-manifest-provider /
  x-manifest-auth-type replay rewrite is gone, and the recorder writes no
  repair columns or auto_fixed rows.
- Analytics: move the generic reliability metrics to
  /api/v1/overview/reliability/{stats,timeseries,per-agent,per-provider,
  per-model} without the Autofix fields; drop the healed disposition, the
  auto_fixed counter, the autofix trigger filter and the connection
  breakdown's Autofix attempts.
- Remove /autofix/status, /autofix/enable-all, the per-harness
  /routing/:agent/autofix endpoints, the autofix_enabled create field, the
  MCP Autofix tools and the CLI `routing autofix` command and `--autofix`.
- Schema: DropAutofixSchema1803400000000 drops the autofix columns, check
  constraint and indexes (lock_timeout, IF EXISTS, full revert).
  1802200000000 now runs its self-hosted no-op path everywhere.
- Shared: drop deriveAutofixStatus, AUTOFIX_STATUSES and auto_fixed.
- Delete the unused doctor-release email.
- Frontend: call the neutral reliability paths and drop the temporary
  autofix_enabled opt-out, the duplicate PATCH and the healed fold.
- Docs: move the PoC spec to docs/referencia/, remove AUTOFIX_* from the
  guide, .env.example and the Docker README.

BREAKING CHANGE: the Autofix API, analytics paths, MCP tools and CLI
command are removed; the reliability analytics moved to
/api/v1/overview/reliability/*.

Closes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove phone-home from the backend and the CLI

Thinker makes no outbound call of its own. Remove every upstream
phone-home from the backend and the CLI:

- usage telemetry (telemetry/, InstallIdService, PayloadBuilderService,
  the InstallMetadata entity) and the CLI usage spool
  (packages/cli/src/telemetry.ts);
- the daily update check (version/, GET /api/v1/version, the GitHub
  releases URL);
- the discovery forwarding to blue.manifest.build (discovery/,
  POST /api/v1/discovery/complete);
- Sentry (instrument.ts, sentry/, SentryModule/SentryGlobalFilter,
  the @sentry/nestjs dependency);
- the hosted Wingman origin in CORS and CSP frame-src; frame-src is
  'self' in every mode and WINGMAN_CORS_ORIGINS becomes
  THINKER_CORS_ORIGINS with the same meaning;
- the modelparams.dev fetch on boot and hourly cron; parameter specs
  come only from the bundled modelparams package snapshot.

DropInstallMetadata1803500000000 drops the install_metadata table
(lock_timeout, IF EXISTS, revert recreates it empty).

The removed variables leave docker-compose, the .env examples, the
Docker README, the deploy templates and the developer guide.

A new e2e guard fails if the backend, CLI or shared source names a
removed destination again, or if a production-mode boot of the real
server fetches or resolves anything outside the ADR 0003 catalogs.

BREAKING CHANGE: GET /api/v1/version and POST /api/v1/discovery/complete
are gone, the hosted Wingman origin is no longer allowed by CORS, and
WINGMAN_CORS_ORIGINS is renamed THINKER_CORS_ORIGINS.

Closes #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): default to a local gateway instead of Manifest Cloud

With no --url, MANIFEST_URL or stored login, the CLI targeted
https://app.manifest.build, so a MANIFEST_API_KEY set without a host
went to Manifest's cloud. DEFAULT_URL is now http://localhost:2099, the
default Thinker port, and the invalid-URL hint and README follow.

The backend's 404 hint for /chat/completions told callers to point
their baseURL at https://app.manifest.build/v1; it now refers to this
gateway.

The egress guard now fails on app.manifest.build or
gateway.manifest.build anywhere in the CLI or shared source (tests
included), on a new backend production file naming them, on a CLI
default that is not local, and on a boot request to either host. The
three remaining backend files (Cloud trusted origins, the dashboard
link fallback and the OG-tag placeholder) are pinned for #18, #22 and
#25 and may only shrink.

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend): single self-hosted mode, drop MANIFEST_MODE

Thinker has one deployment mode. The backend no longer decides between
"cloud" and "self-hosted" from MANIFEST_MODE or container markers
(/.dockerenv, /run/.containerenv, KUBERNETES_SERVICE_HOST); it always
keeps the former self-hosted behavior, on any runtime:

- built-in local providers (Ollama, LM Studio, llama.cpp) are always
  available: the cloud-only filters, the M303 rejection and its error
  code are gone;
- custom-provider URLs validate with allowPrivate: LAN, loopback and
  http:// destinations are a feature, http:// to a public host is still
  refused, and cloud metadata endpoints stay blocked (now also by name:
  metadata.google.internal, metadata.goog, metadata, instance-data);
- request recordings fall back to the filesystem without S3 credentials;
- the boot backfills always run on the app's DATABASE_URL connection;
- GET /api/v1/setup/status drops isSelfHosted and always probes Ollama.

The CIMD client_id fetch does NOT inherit the old self-hosted allowance:
that URL comes from an unauthenticated MCP client, so it is public-only
by default (DNS failure is an error), with THINKER_CIMD_ALLOW_PRIVATE=true
as the opt-in for LAN-hosted clients.

auth.instance.ts no longer trusts the Manifest Cloud hosts
(app/gateway.manifest.build) as origins or MCP resources; MCP is bound to
the canonical BETTER_AUTH_URL origin only, and the file leaves the
no-phonehome allow-list.

detect-self-hosted.ts becomes container-host.ts with only
getContainerHostAlias(). Unit tests no longer depend on /.dockerenv.

Closes #18
Closes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(backend): pin custom-provider connections against DNS rebinding

Review of #71 (S1): with allowPrivate now constant for custom providers,
a DNS failure passed validatePublicUrl, and fetch then resolved the name
again with no check. A hostile resolver could answer a public address
(or fail) during validation and 169.254.169.254 at connect time, so the
probe and the forward could reach cloud metadata.

- New common/utils/ssrf-safe-fetch.ts: undici fetch with a shared Agent
  whose connect.lookup resolves once, refuses the connection when any
  answer is a metadata address, and hands the socket only validated
  addresses. Metadata literals and names are refused before any socket.
  A DNS failure fails the request.
- The custom-provider probe and the provider-client forward for
  user-supplied endpoints (requiresSsrfRevalidation) use it.
- validatePublicUrl treats a DNS failure as an error even with
  allowPrivate (a .invalid host is refused).
- S2: block the AWS container credential endpoints 169.254.170.2 (ECS),
  169.254.170.23 and fd00:ec2::23 (EKS Pod Identity).
- N2: classify IMDS embedded in IPv6 (IPv4-compatible, NAT64
  64:ff9b::/96, 6to4, Teredo) as metadata; IPv6 metadata matches any
  textual form.
- undici 6.29.0 becomes a backend dependency (the root override already
  pins undici ^6); Node's built-in fetch cannot take a foreign Agent
  across Node versions, so undici's own fetch is used.

Tests: unit specs for the lookup and the classifier, and an e2e where a
hostname resolves to a public address at validation and to
169.254.169.254 at connect: the probe and the forward are refused and a
listener standing in for the metadata address receives 0 connections
(it receives them when pinning is removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the billing drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the SaaS drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the Autofix drop e2e through the database guard

The DropAutofixSchema e2e spec opens its own DataSource with dropSchema,
so it now takes its URL from resolveE2eDatabaseUrl() like the other
migration specs instead of falling back to a default localhost database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the install-metadata drop e2e through the database guard

The DropInstallMetadata migration spec and the no-phone-home egress spec
both open a dropSchema connection. Resolve their URL with
resolveE2eDatabaseUrl() from test/e2e-db-guard instead of falling back to
localhost:5432, so neither can wipe a database that is not an explicit,
disposable test database (#67, #69).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): drop MANIFEST_MODE from local-runner specs and fix the local log flake

- Remove the MANIFEST_MODE=selfhosted setup that #100, #103 and #104 added
  to provider-client.local-servers.spec.ts, local-providers.e2e-spec.ts,
  reserved-local-runner-aliases.e2e-spec.ts and
  playground-request-headers.e2e-spec.ts. There is one mode now, so the
  variable does nothing.
- local-providers.e2e-spec.ts: "logs local requests under the canonical
  provider at zero cost" read rows[0] of every llama.cpp row for the agent,
  with no ORDER BY, while earlier tests also log llama.cpp requests and a
  row starts as `pending` before its usage lands. Tag the request with its
  own traceparent and wait for that one settled row. Closes that part of
  #101.
- mcp-cimd-ssrf.e2e-spec.ts: take the database from resolveE2eDatabaseUrl()
  instead of falling back to localhost:5432 before dropping the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): escape host names fully in the egress spec patterns

CodeQL flagged js/incomplete-sanitization: only dots were escaped before
building a RegExp from the host constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
patrickbrandao added a commit that referenced this pull request Oct 11, 2026
…rovider (#39) (#72)

* refactor(frontend): remove the SaaS layer from the dashboard

Thinker is self-hosted only, so the inherited Manifest Cloud UI goes.
The frontend leaves first so no screen breaks when #14/#15 drop the
matching backend endpoints.

- Billing/plans: delete PlanPicker, UpgradeSuccessModal,
  UsageLimitBanner, the /upgrade page, the Register ?step=plan picker,
  the plan stores, the plan-based range locks on the analytics pages,
  the plan-limit upgrade CTAs in the request views, the Account billing
  and usage-email sections, and the @better-auth/stripe client plugin
  and dependency.
- API Bot: delete the banner, the sidebar card, the dithered ground,
  their stylesheet and the SVG asset.
- GitHub stars: drop the header star button and its /api/v1/github call.
- Free Models: delete the page, its route, its API client and the assets
  only it used.
- Managed Gemini Free: drop the provider entry, icon alias, background
  /ensure provisioning call and the Calendly key URL.
- Cloud branches: checkIsSelfHosted() is gone and every screen behaves
  as self-hosted (Local providers always listed, local logs copy, CLI
  login pinned to this host, discovery after signup and setup). Drop the
  Manifest Cloud origin mapping, the cloud email card, the
  VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it.
- Help: drop the Manifest Calendly booking link.
- Remove the dead CSS for all of the above, update the tests and the
  inherited dev guide.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove billing, Stripe and plan limits

Billing, Stripe and the Free/Pro plans belong to the SaaS layer that
Thinker drops (docs/decisoes-iniciais.md section 3).

- Delete packages/backend/src/billing (PlanService, billing controller,
  billing emails, request quota window, subscription status) and drop
  BillingModule from the app, proxy and analytics modules.
- Proxy: remove the per-request plan gate (assertWithinRequestLimit) and
  the PLAN_LIMIT_REQUESTS / M204 handling in the controller and the
  exception filter. The proxy no longer answers 402 on its own; a 402
  follows the generic error path.
- Auth: remove the @better-auth/stripe plugin; drop the stripe and
  @better-auth/stripe dependencies from the backend.
- Recording retention: keep only the global path (365-day default or
  REQUEST_RECORDING_RETENTION_DAYS); per-plan retention is gone.
- Tenant entity: drop limit_overrides and billing_email_preferences;
  delete the TenantRequestUsage entity.
- Shared: delete plan-limits and its exports.
- Remove the STRIPE_* / PLAN_LIMIT_* / PLAN_REQUEST_QUOTA_RESET_AT
  variables from .env.example.
- New migration DropBillingSchema1803200000000 drops the quota trigger
  and function, requests.quota_counted, tenant_request_usage,
  billing_email_logs and the two tenant columns. The old
  AddTenantRequestUsage migration keeps only its self-hosted path
  (a no-op up) so it no longer imports deleted billing code.

/api/v1/billing/* no longer exists, and the STRIPE_* and PLAN_LIMIT_*
variables are ignored if set.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(frontend): remove phone-home and Autofix from the dashboard

Thinker makes no outbound calls of its own, and Autofix depended on
Manifest's hosted Phoenix service. The dashboard drops both before the
backend modules go in #16 and #17.

- Update check: VersionIndicator now only shows the build-time
  __MANIFEST_VERSION__ badge. It makes no request, shows no "new version"
  link to the manifest.build changelog and has no dismiss state.
  services/api/version.ts and its test are gone.
- Discovery: delete the post-signup questionnaire (page, service, styles,
  route, tests) and the pending-discovery redirects in AuthGuard and
  GuestGuard. First-admin setup now lands on / (Overview), signup on
  /welcome, and social signup also goes to /welcome. A leftover
  manifest_discovery_pending_* key from an older build no longer
  redirects anyone.
- Wingman: delete the dev drawer, wingman-dev-proxy.ts, the
  VITE_WINGMAN_* / __WINGMAN_URL__ / __DEV_MODE__ build constants, the
  App and Welcome mounts, the drawer CSS and the CONTRIBUTING section.
- Autofix: delete AutofixKpiCards, SettingsAutofixSection,
  NotificationBell (it only nagged about inactive Autofix), the Autofix
  switch and Manifest Terms/Privacy line on harness creation (modal and
  Welcome), the autofix badges, trigger filter, Phoenix panels, sibling
  links and status labels in the Requests views, the "Autofixed attempts"
  connection card, the Doctor tooltip variant, the healed chart series,
  the autofix fields in services/api/*, the two public PNGs, and the
  dead CSS.
- The generic reliability metrics that the backend still serves under
  the legacy /overview/autofix-* paths (success rate, fallback
  recoveries, request status over time, per harness/provider/model
  reliability) stay. They are now read through neutral clients
  (getReliabilityStats, getRequestStatusTimeseries, RELIABILITY_PATHS)
  and shown by ReliabilityKpiCards. "Self-healed" becomes "Recovered" and
  counts fallback recoveries only.
- Update the tests, the design-system registry and the inherited dev
  guide.

Closes #13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): opt new harnesses out of legacy Autofix until #16

Review follow-up for #57.

- A harness stored without autofix_enabled inherits the backend default.
  The backend turns Autofix ON when it detects no container (bare-metal
  npm start, dev runs), and the dashboard no longer has a switch to turn
  it off. Until #16 removes Autofix from the backend:
  - createAgent always sends autofix_enabled: false, which covers the
    Connect Harness modal, Welcome and ConnectProvider;
  - duplicateAgent follows the copy with PATCH
    /routing/:agent/autofix {enabled:false}, because the duplicate
    endpoint has no field for it.
  Both live in one commented block in services/api/agents.ts, with
  tests on the request payloads.
- The request-status timeseries client folds a legacy `healed` key into
  `success`. The backend's success rate already counts those requests
  as successes, so the bars still sum to the request total, Success +
  Recovered + Error matches the Success rate card, and Recovered stays
  fallback-only, like its KPI.
- The Requests page rewrites a stale ?trigger= value (for example
  ?trigger=autofix) on load to the filter it actually applies.
- Add AuthGuard tests for /cli/auth, with and without a session.
- Correct the dev guide: the self-hosted default is OFF only in a
  container or with MANIFEST_MODE=selfhosted. Document
  AUTOFIX_GLOBAL_ENABLED=false as the operator workaround for existing
  harnesses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(backend): remove waitlist, CRM, Gemini Free, free models, GitHub stars and error pages

Remove the Manifest Cloud and marketing surfaces from the backend:

- waitlist/ (pivot and Autofix claims) and its open-CORS pivot route
  (applyPivotClaimCors, PIVOT_CLAIM_PATH, PIVOT_CLAIM_CLOUD_ORIGIN).
- crm-metrics/ and CRM_METRICS_SECRET.
- The managed Gemini Free provider: routing/managed-free-provider/,
  common/constants/managed-free-providers.ts, the CREDITS_* variables, and
  its entries in provider-endpoints, provider-client, provider.service,
  provider-model-fetcher and routing.module. No other provider's endpoint,
  prefix handling, fallback order or tier resolution changes.
- free-models/ (sync from mnfst/awesome-free-llm-apis), github/ (stars from
  api.github.com) and error-pages/ (public and Peacock push routes, seeders,
  discovery), MANIFEST_PUBLIC_STATS and ERROR_PAGE_PUSH_SECRET.
- gemini-free and the managedFree flag from manifest-shared, and the
  regenerated CLI provider catalog.
- The plan_request_limit_exceeded error class left over from #14.

DropSaasFeatureSchema1803300000000 sets a transaction-local lock_timeout,
drops waitlist_claims, public_error_pages and the cloud-only
IDX_requests_autofix_healed index, and deletes every gemini-free connection
with its agent grants. Request history keeps its provider label, and routes
that still name gemini-free resolve as unavailable and fall through to their
fallbacks, as after a disconnect. down() restores the two tables as the
self-hosted path created them.

Closes #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove Autofix and the Phoenix healer from the backend

Autofix sent failing requests to Manifest's hosted healer
(autofix.manifest.build) and pinged it at every production boot. Thinker
makes no such call (#16). The local self-heal of v0.6 will build on the
fallback chain on its own terms.

- Delete routing/autofix (AutofixService, healing clients, health probe,
  observation reporter, Phoenix types and vendored contract) and the
  contract:refresh script with its ajv/js-yaml devDependencies.
- Proxy: drop the heal-once branch from the primary and fallback hops and
  the wire-body retry hook from ForwardResult. A failed request now goes
  straight to shouldTriggerFallback, exactly as for an agent that never
  opted in. The primary is always attempt 1, the x-manifest-provider /
  x-manifest-auth-type replay rewrite is gone, and the recorder writes no
  repair columns or auto_fixed rows.
- Analytics: move the generic reliability metrics to
  /api/v1/overview/reliability/{stats,timeseries,per-agent,per-provider,
  per-model} without the Autofix fields; drop the healed disposition, the
  auto_fixed counter, the autofix trigger filter and the connection
  breakdown's Autofix attempts.
- Remove /autofix/status, /autofix/enable-all, the per-harness
  /routing/:agent/autofix endpoints, the autofix_enabled create field, the
  MCP Autofix tools and the CLI `routing autofix` command and `--autofix`.
- Schema: DropAutofixSchema1803400000000 drops the autofix columns, check
  constraint and indexes (lock_timeout, IF EXISTS, full revert).
  1802200000000 now runs its self-hosted no-op path everywhere.
- Shared: drop deriveAutofixStatus, AUTOFIX_STATUSES and auto_fixed.
- Delete the unused doctor-release email.
- Frontend: call the neutral reliability paths and drop the temporary
  autofix_enabled opt-out, the duplicate PATCH and the healed fold.
- Docs: move the PoC spec to docs/referencia/, remove AUTOFIX_* from the
  guide, .env.example and the Docker README.

BREAKING CHANGE: the Autofix API, analytics paths, MCP tools and CLI
command are removed; the reliability analytics moved to
/api/v1/overview/reliability/*.

Closes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend)!: remove phone-home from the backend and the CLI

Thinker makes no outbound call of its own. Remove every upstream
phone-home from the backend and the CLI:

- usage telemetry (telemetry/, InstallIdService, PayloadBuilderService,
  the InstallMetadata entity) and the CLI usage spool
  (packages/cli/src/telemetry.ts);
- the daily update check (version/, GET /api/v1/version, the GitHub
  releases URL);
- the discovery forwarding to blue.manifest.build (discovery/,
  POST /api/v1/discovery/complete);
- Sentry (instrument.ts, sentry/, SentryModule/SentryGlobalFilter,
  the @sentry/nestjs dependency);
- the hosted Wingman origin in CORS and CSP frame-src; frame-src is
  'self' in every mode and WINGMAN_CORS_ORIGINS becomes
  THINKER_CORS_ORIGINS with the same meaning;
- the modelparams.dev fetch on boot and hourly cron; parameter specs
  come only from the bundled modelparams package snapshot.

DropInstallMetadata1803500000000 drops the install_metadata table
(lock_timeout, IF EXISTS, revert recreates it empty).

The removed variables leave docker-compose, the .env examples, the
Docker README, the deploy templates and the developer guide.

A new e2e guard fails if the backend, CLI or shared source names a
removed destination again, or if a production-mode boot of the real
server fetches or resolves anything outside the ADR 0003 catalogs.

BREAKING CHANGE: GET /api/v1/version and POST /api/v1/discovery/complete
are gone, the hosted Wingman origin is no longer allowed by CORS, and
WINGMAN_CORS_ORIGINS is renamed THINKER_CORS_ORIGINS.

Closes #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): default to a local gateway instead of Manifest Cloud

With no --url, MANIFEST_URL or stored login, the CLI targeted
https://app.manifest.build, so a MANIFEST_API_KEY set without a host
went to Manifest's cloud. DEFAULT_URL is now http://localhost:2099, the
default Thinker port, and the invalid-URL hint and README follow.

The backend's 404 hint for /chat/completions told callers to point
their baseURL at https://app.manifest.build/v1; it now refers to this
gateway.

The egress guard now fails on app.manifest.build or
gateway.manifest.build anywhere in the CLI or shared source (tests
included), on a new backend production file naming them, on a CLI
default that is not local, and on a boot request to either host. The
three remaining backend files (Cloud trusted origins, the dashboard
link fallback and the OG-tag placeholder) are pinned for #18, #22 and
#25 and may only shrink.

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend): single self-hosted mode, drop MANIFEST_MODE

Thinker has one deployment mode. The backend no longer decides between
"cloud" and "self-hosted" from MANIFEST_MODE or container markers
(/.dockerenv, /run/.containerenv, KUBERNETES_SERVICE_HOST); it always
keeps the former self-hosted behavior, on any runtime:

- built-in local providers (Ollama, LM Studio, llama.cpp) are always
  available: the cloud-only filters, the M303 rejection and its error
  code are gone;
- custom-provider URLs validate with allowPrivate: LAN, loopback and
  http:// destinations are a feature, http:// to a public host is still
  refused, and cloud metadata endpoints stay blocked (now also by name:
  metadata.google.internal, metadata.goog, metadata, instance-data);
- request recordings fall back to the filesystem without S3 credentials;
- the boot backfills always run on the app's DATABASE_URL connection;
- GET /api/v1/setup/status drops isSelfHosted and always probes Ollama.

The CIMD client_id fetch does NOT inherit the old self-hosted allowance:
that URL comes from an unauthenticated MCP client, so it is public-only
by default (DNS failure is an error), with THINKER_CIMD_ALLOW_PRIVATE=true
as the opt-in for LAN-hosted clients.

auth.instance.ts no longer trusts the Manifest Cloud hosts
(app/gateway.manifest.build) as origins or MCP resources; MCP is bound to
the canonical BETTER_AUTH_URL origin only, and the file leaves the
no-phonehome allow-list.

detect-self-hosted.ts becomes container-host.ts with only
getContainerHostAlias(). Unit tests no longer depend on /.dockerenv.

Closes #18
Closes #46

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backend): add THINKER_CATALOG_SYNC and gate the OpenCode Go doc on a provider

Implements ADR 0003 (#39):

- THINKER_CATALOG_SYNC=false (also 0/no/off) turns off all three public
  catalog fetches: no boot fetch, the daily crons and the pricing/model
  refresh paths become no-ops, and the OpenCode Go catalog returns []
  without fetching. OpenRouter and models.dev stay on by default.
- OpencodeGoCatalogService only warms the raw.githubusercontent.com doc at
  boot when a tenant_providers row for OpenCode Go (id or alias) exists.
  Every other caller is already scoped to an OpenCode Go provider, so a
  newly registered provider loads the doc lazily through its discovery.
- pricing-health and pricing/refresh report sync_enabled, and the routing
  status (REST and MCP) no longer blames the OpenRouter cache, which is
  empty by design with the sync off.
- The egress e2e boots the real server three times: no OpenCode Go
  provider (OpenRouter and models.dev fetched, no raw.githubusercontent.com),
  provider + THINKER_CATALOG_SYNC=false (no remote fetch at all), and
  provider + default (the doc is warmed).
- Documents the variable in both .env.example files, the compose file,
  DOCKER_README and the dev guide, including what pricing loses when the
  sync is off.

Closes #39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(backend): pin custom-provider connections against DNS rebinding

Review of #71 (S1): with allowPrivate now constant for custom providers,
a DNS failure passed validatePublicUrl, and fetch then resolved the name
again with no check. A hostile resolver could answer a public address
(or fail) during validation and 169.254.169.254 at connect time, so the
probe and the forward could reach cloud metadata.

- New common/utils/ssrf-safe-fetch.ts: undici fetch with a shared Agent
  whose connect.lookup resolves once, refuses the connection when any
  answer is a metadata address, and hands the socket only validated
  addresses. Metadata literals and names are refused before any socket.
  A DNS failure fails the request.
- The custom-provider probe and the provider-client forward for
  user-supplied endpoints (requiresSsrfRevalidation) use it.
- validatePublicUrl treats a DNS failure as an error even with
  allowPrivate (a .invalid host is refused).
- S2: block the AWS container credential endpoints 169.254.170.2 (ECS),
  169.254.170.23 and fd00:ec2::23 (EKS Pod Identity).
- N2: classify IMDS embedded in IPv6 (IPv4-compatible, NAT64
  64:ff9b::/96, 6to4, Teredo) as metadata; IPv6 metadata matches any
  textual form.
- undici 6.29.0 becomes a backend dependency (the root override already
  pins undici ^6); Node's built-in fetch cannot take a foreign Agent
  across Node versions, so undici's own fetch is used.

Tests: unit specs for the lookup and the classifier, and an e2e where a
hostname resolves to a public address at validation and to
169.254.169.254 at connect: the probe and the forward are refused and a
listener standing in for the metadata address receives 0 connections
(it receives them when pinning is removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the billing drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the SaaS drop e2e through the database guard

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the Autofix drop e2e through the database guard

The DropAutofixSchema e2e spec opens its own DataSource with dropSchema,
so it now takes its URL from resolveE2eDatabaseUrl() like the other
migration specs instead of falling back to a default localhost database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): route the install-metadata drop e2e through the database guard

The DropInstallMetadata migration spec and the no-phone-home egress spec
both open a dropSchema connection. Resolve their URL with
resolveE2eDatabaseUrl() from test/e2e-db-guard instead of falling back to
localhost:5432, so neither can wipe a database that is not an explicit,
disposable test database (#67, #69).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): drop MANIFEST_MODE from local-runner specs and fix the local log flake

- Remove the MANIFEST_MODE=selfhosted setup that #100, #103 and #104 added
  to provider-client.local-servers.spec.ts, local-providers.e2e-spec.ts,
  reserved-local-runner-aliases.e2e-spec.ts and
  playground-request-headers.e2e-spec.ts. There is one mode now, so the
  variable does nothing.
- local-providers.e2e-spec.ts: "logs local requests under the canonical
  provider at zero cost" read rows[0] of every llama.cpp row for the agent,
  with no ORDER BY, while earlier tests also log llama.cpp requests and a
  row starts as `pending` before its usage lands. Tag the request with its
  own traceparent and wait for that one settled row. Closes that part of
  #101.
- mcp-cimd-ssrf.e2e-spec.ts: take the database from resolveE2eDatabaseUrl()
  instead of falling back to localhost:5432 before dropping the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): escape host names fully in the egress spec patterns

CodeQL flagged js/incomplete-sanitization: only dots were escaped before
building a RegExp from the host constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remover phone-home e Autofix do dashboard

1 participant