Skip to content

fix(tools-mcps): instalação migrada não manda o segredo inline desvinculado (CRM-877) - #84

Merged
daniloleonecarneiro merged 4 commits into
developfrom
fix/CRM-877-header-inline-apos-desvincular
Nov 5, 2026
Merged

daniloleonecarneiro merged 4 commits into
developfrom
fix/CRM-877-header-inline-apos-desvincular

Conversation

@daniloleonecarneiro

@daniloleonecarneiro daniloleonecarneiro commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A integration_credentials:migrate do CRM copia o segredo inline para o cofre, liga a referência e mantém o valor na linha. Quando o usuário desvinculava a credencial, o processor via a referência vazia e voltava a autenticar com o valor antigo, que o usuário considerava aposentado.

Agora, na instalação com a marca INTEGRATION_CREDENTIALS_MIGRATED (a mesma que o CRM grava e lê para os bots):

  • Tool HTTP e MCP custom: header de autenticação sem credential_refs não é enviado. A decisão é por header: authorization, x-api-key, api-key, apikey e x-auth-token, a mesma lista que a migração importa. Os outros headers seguem.
  • Knowledge Nexus: sem credential_id, a nexus_api_key inline não é usada e a tool não é montada (fica o aviso de campo faltando).
  • Agente externo (dify, flowise, n8n, openai): sem credential_id, o build falha dizendo que não há credencial vinculada, em vez de autenticar com o inline.

Sem a marca, ou com erro na leitura, nada muda: o inline continua sendo o fallback. A marca é lida uma vez por build de tools e uma vez por build de MCPs, numa sessão própria, e só quando existe segredo inline sem referência.

Também corrigido, porque o fix dependia disso: as expansões que o processor faz a partir dos ids não levavam credential_refs:

  • tool por custom_tool_ids e convert_to_http_tool;
  • MCP custom expandido pelo get_agent em custom_mcp_servers, que o build_lazy_tools conectava com os headers crus.

Nesses caminhos o cofre nunca era consultado, e a tool ou o MCP vinculado autenticava com o inline.

Por que a marca e não uma contagem: com o delete real das credenciais, contar linhas com imported_from ou segredos pendentes não separa "migrou e depois desvinculou" de "nunca migrou". A marca só é gravada pela task de migração e nada a desfaz.

Test plan

  • pytest tests/unit --ignore=tests/unit/test_exception_handlers.py (como no CI): 747 passed
  • tests/unit/services/test_unlinked_inline_auth.py (novo): 49 passed
  • Mutações, todas vermelhas:
    • a string "true" contando como migrado
    • erro de leitura contando como migrado
    • outro campo JSON na query
    • marca ignorada
    • credential_refs fora da tool por id, do convert_to_http_tool e da expansão do MCP
    • entrada expandida do MCP sem passar pelo cofre
    • checagem do Nexus removida
    • checagem do agente externo removida
    • remoção dos headers tirada do MCP e de cada um dos dois builders de tool
    • leitura sem memo
    • builders sem compartilhar a leitura
  • Ao vivo, com linhas reais no Postgres da stack e um servidor local que registra o Authorization recebido:
sem marca com marca
develop: tool desvinculada / vinculada Bearer antigo / Bearer antigo Bearer antigo / Bearer antigo
branch: tool desvinculada / vinculada Bearer antigo / Bearer do-cofre nenhum / Bearer do-cofre
develop: MCP desvinculado (ids → get_agent → build_lazy_tools, cada conexão) Bearer antigo, Bearer antigo
branch: o mesmo MCP nenhum, nenhum
develop: agente externo dify (RunnerUtils.get_and_build_agent) / Nexus monta com o inline / tool montada monta com o inline / tool montada
branch: o mesmo monta com o inline / tool montada erro "no credential linked" / sem tool
  • Fail-closed ao vivo: com a marca gravada e um papel sem permissão em installation_configs (permission denied), a tool desvinculada mandou Bearer antigo.

Linhas, marca e papel temporário apagados no fim.

Notas

  • Instalação que migrou antes da marca existir só passa a ter esse comportamento depois de rodar integration_credentials:record_migration (CRM). Até lá, o inline continua valendo.
  • installation_configs é tabela global, sem RLS; a leitura não depende do tenant da sessão.
  • Fora do escopo: o ElevenLabs também é importado pela migração, mas o processor lê a apiKey inline e não consulta o cofre.

…usuário desvinculou (CRM-877)

A migração das credenciais copia o segredo inline para o cofre e mantém o
valor na linha. Sem referência ao cofre, o processor voltava a usar esse
valor. Com a marca INTEGRATION_CREDENTIALS_MIGRATED gravada pelo CRM:

- tool e MCP custom não mandam header de autenticação sem credential_refs;
- o Knowledge Nexus sem credential_id não monta a tool;
- agente externo sem credential_id falha dizendo que a credencial foi
  desvinculada.

Sem a marca, ou com erro na leitura, o inline continua valendo.

A tool anexada por id não levava o credential_refs da linha ao builder: o
cofre nunca era consultado e a tool vinculada autenticava com o inline.
@sourcery-ai

sourcery-ai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR makes migrated installations honor credential unlinking by retiring unreferenced inline auth secrets, while retaining inline fallback before migration or when migration-state reads fail; it also fixes credential-reference propagation so linked tools continue using the vault and adds comprehensive regression coverage.

Sequence diagram for migrated tool authentication

sequenceDiagram
    participant Builder as ToolBuilder
    participant Policy as retire_unreferenced_auth_headers
    participant Config as installation_integration_migrated
    participant Vault as resolve_credential_refs
    participant Server as HTTP_or_MCP_Server

    Builder->>Policy: retire_unreferenced_auth_headers(headers, credential_refs, migrated)
    alt Auth header has no credential reference
        Policy->>Config: installation_integration_migrated()
        Config-->>Policy: true
        Policy-->>Builder: headers without unlinked auth header
    else Credential reference exists
        Policy-->>Builder: headers retained
        Builder->>Vault: resolve_credential_refs(credential_refs)
        Vault-->>Builder: vault secret
    end
    Builder->>Server: send request with resulting headers
Loading

Flow diagram for inline secret handling after migration

flowchart TD
    A[Inline secret or auth header present] --> B{credential reference exists?}
    B -->|Yes| C[resolve_credential_refs]
    C --> D[Use vault credential]
    B -->|No| E{installation_integration_migrated}
    E -->|No or read error| F[Keep inline fallback]
    E -->|Yes| G[Retire inline auth secret]
    G --> H[Tool-specific result]
    H --> I[HTTP or MCP: omit auth header]
    H --> J[Knowledge Nexus: do not build tool]
    H --> K[External agent: raise ValueError]
Loading

File-Level Changes

Change Details Files
Stop using inline authentication secrets after an installation has recorded credential migration when no vault credential remains linked.
  • Add fail-closed detection of the exact JSON-boolean migration flag using an isolated database session.
  • Remove unreferenced authentication headers while preserving non-auth headers, pre-migration fallback behavior, and vault-linked headers.
  • Reject unlinked inline secrets for external agents and suppress unlinked Knowledge Nexus tools.
  • Propagate credential references through custom-tool lookup and HTTP-tool conversion paths.
src/services/adk/integration_credentials.py
src/services/adk/custom_tools.py
src/services/adk/mcp_service.py
src/services/adk/tool_builder.py
src/services/custom_tool_service.py
Add focused coverage for migration-state handling and all affected tool and agent authentication paths.
  • Test exact flag semantics, read failures, and isolated-session behavior.
  • Test per-header retirement across HTTP, custom MCP, ID-attached tools, and conversion paths.
  • Test vault-linked credentials, pre-migration fallback, external-agent failures, and Knowledge Nexus tool suppression.
tests/unit/services/test_unlinked_inline_auth.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/services/adk/mcp_service.py" line_range="1007" />
<code_context>
+    )
+
     credential_refs = tool_config.get("credential_refs") or {}
+    headers = retire_unreferenced_auth_headers(
+        headers, credential_refs, migrated=installation_integration_migrated
+    )
</code_context>
<issue_to_address>
**Custom MCP servers keep unlinked secrets**

When a migrated installation supplies a custom server through `custom_mcp_servers`, `build_lazy_tools` passes that server directly to `mcp_context` and `LazyMCPTool` without calling `_resolve_mcp_headers`, so unlinked inline auth headers are still sent.

Route custom server configurations through `_resolve_mcp_headers` before passing them to `mcp_context` or `LazyMCPTool`.
</issue_to_address>

### Comment 2
<location path="src/services/adk/custom_tools.py" line_range="164-166" />
<code_context>
+    )
+
     credential_refs = tool_config.get("credential_refs") or {}
+    headers = retire_unreferenced_auth_headers(
+        headers, credential_refs, migrated=installation_integration_migrated
+    )
     if not credential_refs:
         return headers
</code_context>
<issue_to_address>
**Migration status is queried per tool**

When an agent has multiple custom HTTP or MCP tools with nonempty auth headers lacking references, each tool with an unreferenced auth header calls `installation_integration_migrated`, which opens a new session and executes a database query. Building an agent with many such tools therefore adds one serial database round trip per tool, increasing build latency and database load.

Read and cache the migration status once per agent/tool build, then pass that result to each header-retirement check.

Also at `src/services/adk/tool_builder.py:75-77`, `src/services/adk/mcp_service.py:1007-1009`.
</issue_to_address>

### Comment 3
<location path="tests/unit/services/test_unlinked_inline_auth.py" line_range="58-62" />
<code_context>
+        assert integration_migrated(_db_answering(raises=RuntimeError("boom"))) is False
+
+    def test_reads_the_flag_the_crm_migration_writes(self):
+        db = _db_answering((True,))
+        integration_migrated(db)
+
+        sql, params = db.execute.call_args.args
+        assert "installation_configs" in str(sql)
+        assert params == {"name": "INTEGRATION_CREDENTIALS_MIGRATED"}
+
</code_context>
<issue_to_address>
**Wrong flag query passes test**

When the migration-status query selects a different or incorrect JSON field, `test_reads_the_flag_the_crm_migration_writes` supplies a mocked `(True,)` row regardless of the SQL, then checks only the table name and bind parameter. A query that selects the wrong JSON field still passes, so a real migrated installation can be treated as not migrated and keep sending unlinked secrets.

Assert the expected SQL expression or test the query against a database fixture containing the migration flag.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 3 findings to address first, and if a credential reference is missing or mismatched after migration, this change strips the inline auth header or makes an external-agent credential unusable, so integrations can fail until the configuration is corrected. Reverting restores the prior inline-secret behavior, but any outage during that period cannot be undone.

Blocking findings: src/services/adk/mcp_service.py:1007, src/services/adk/custom_tools.py:166, tests/unit/services/test_unlinked_inline_auth.py:62


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/services/adk/mcp_service.py Resolved
Comment thread src/services/adk/custom_tools.py Resolved
…e migration read per build (CRM-877)

get_agent expands custom_mcp_server_ids into custom_mcp_servers entries
carrying only url and raw headers, and build_lazy_tools connected to them
as they were: an unlinked header still went out, and a linked one never
reached the vault. The expansion now carries credential_refs and those
entries resolve through _resolve_mcp_headers.

The migration record is read once per tool or MCP build instead of once
per tool.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sourcery assessment

Approved.

@daniloleonecarneiro
daniloleonecarneiro merged commit c11f9f6 into develop Nov 5, 2026
5 checks passed
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.

1 participant