Skip to content

[ENG-2422] Tell a web turn where credentials go instead of asking for them in chat - #466

Open
tino097 wants to merge 9 commits into
stagingfrom
tino097/eng-2422-a-web-user-cannot-create-a-connector-credential-at-all-so
Open

[ENG-2422] Tell a web turn where credentials go instead of asking for them in chat#466
tino097 wants to merge 9 commits into
stagingfrom
tino097/eng-2422-a-web-user-cannot-create-a-connector-credential-at-all-so

Conversation

@tino097

@tino097 tino097 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

https://linear.app/mindsdb/issue/ENG-2422/a-web-user-cannot-create-a-connector-credential-at-all-so-the-only

What changed

  • stops asking the user for credentials in chat, and forbids storing a pasted
    one
  • tells a cloud turn where credentials go, since nothing on this surface could
  • closes the two solicitations the first pass missed, and tests the property
  • points the referral at the entry that exists, and states the desktop parity
  • keeps this change to the web turn and moves the rule into the pod suffix
  • makes the rule outrank what renders after it, not only what came before: the
    remembered-rules snapshot is appended after the suffix, so an org rule asking
    for an API key was never countermanded by a guard claiming precedence over
    earlier text alone
  • covers the channels that instruct from outside the prompt: recall_skill is
    allowlisted and the skill tree is the tenant's, so a user-authored skill body
    saying to ask for a key arrives as tool output, outside a guard bounded by
    "in this prompt"

The fifth commit narrows the first four; the last two answer review findings. The net diff against staging is two
files, anton/cloud_turn/session.py and tests/test_cloud_turn_session.py:
the shared base prompt and the connector tool descriptions are byte-identical
to base, so the CLI and the desktop are untouched. Reviewing the net diff is
easier than reading the commits in order.

Notes for the reviewer

  • The whole rule lives in the suffix, on purpose. With the shared prompt
    reverted, nothing else on this surface forbids asking for a credential, tells
    the user to rotate one that arrives anyway, or forbids storing it.
  • The no-storing clause is load-bearing. memorize and
    create_skill_draft are both allowlisted in the pod, memory writes are
    applied org-side and replayed on later turns, and the relay's scrubber
    matches four shapes that a GitHub PAT, a WordPress application password and
    an SMTP password all miss. Without it, "get the secret out of the transcript"
    reads as "store it somewhere else", which would turn one exposed trace into
    permanent exposure.
  • The suffix countermands the shared prompt in words, and cannot do it by
    position.
    The base prompt still lists credentials among the things to ask
    the user for. The suffix does not render last: ChatSystemPromptBuilder.build
    appends the relevance-filtered memory snapshot after it, and a recalled skill
    body never passes through the prompt at all. So the text claims precedence
    over every channel explicitly rather than relying on placement. A test
    asserts the shared invitation is still present, so if it is ever removed this
    override gets revisited rather than lingering.
  • No cache effect. The suffix is two module constants joined at import, so
    it is byte-identical every turn. It lengthens the cached block, which is
    written once and then read at about 0.1x, and adds neither invalidator from
    the prompt-cache investigation. Note for anyone reasoning about placement
    here: the ordering above is not a caching argument. anton places no
    cache_control, and the gateway wraps the whole system string in one block
    with a single breakpoint at its end, so position inside system has no cache
    consequence either way.
  • Known cost: the desktop solicitation stays live. anton/tools.py still
    says to ask for credentials in chat on the console-less path, which is Cowork
    desktop, and ENG-1849 observed five users pasting live credentials there in a
    single week. Deliberately out of scope here; it wants its own change.
  • The referral names the sidebar label, not "Connectors". Connectors also
    titles the Code page whose non-Electron branch saves personal tokens through
    /connectors/connections/validate-and-save, a route with no org-mode
    handling, into a vault no pod turn reads. Pointing a web user there would
    reproduce this dead end one step later. A rename in cowork has no test on
    this side to catch it.
  • It names the desktop app deliberately. An earlier revision forbade that,
    on the grounds that a credential added there is unreachable from a web turn.
    The ticket asks for parity stated plainly and the web UI already points at
    the desktop app in two places, so silence had the agent contradict the
    screen. The caveat the UI omits rides along in the prompt.
  • No surface read. build_cloud_chat_session has one production caller,
    the headless entrypoint, so the suffix is web-only by construction.
    Branching on surface would break the relay's stated invariant that the
    trace block is observability only.
  • Acceptance criteria are partly unmet, by design. Criteria 3 and 4 are
    met. Criterion 1 is met only for the connectors auth actually supports.
    Criterion 2 needs transcript-side redaction and criterion 5 is now ENG-2661,
    which has to close before the ticket can. This PR should not close it.
  • Found while reviewing, not fixed here:
    /connectors/connections/save and /validate-and-save have no org_mode
    branch although five siblings in the same module do, and live callers exist
    in the shell-agnostic renderer tree. An org web user can store a credential
    nothing can read. Worth its own ticket.

How to test

  1. Run the suites below; the prompt-level assertions are the behaviour.
  2. To see the referral as the model does, build a cloud session and render its
    system prompt: the suffix names Connect Apps and Data, names no connector,
    carries the desktop referral with its caveat, and forbids storing a value.
  3. Confirm nothing changed off the web path: git diff staging..HEAD -- anton/core/llm/prompts.py anton/tools.py is empty.

Checks

Check Observed result
uv run --group dev pytest tests/ --ignore=tests/e2e 3098 passed, 31 skipped
uv run --group dev pytest tests/e2e/ 40 passed

Both re-run at the head commit. Every new assertion was also confirmed to
invert at the base revision, so they fail on the original bug rather than
passing vacuously.

🤖 Generated with Claude Code

tino097 and others added 5 commits September 10, 2026 14:45
…sted one

Four strings solicited a chat paste: two base-prompt bullets both surfaces
share, and two on the console-less connect path that Cowork desktop gets. On a
host with no credential tool, chat was the only way to comply.

The base prompt now carries the prohibition alone and names no tool, since
where a credential goes differs per host. Recovery is untouched: mode (a) still
vaults a value that already reached the conversation, which is the only thing
that gets it out of the transcript.

Where nothing can store a credential the rule forbids storing it at all, rather
than listing stores to avoid. The pod allowlists memorize and
create_skill_draft, memory writes are applied and re-sent on later turns, and
the relay scrubber matches four shapes that a GitHub PAT, a WordPress
application password and an SMTP password all miss, so a stored value would
turn one exposed trace into permanent exposure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… could

The pod passed no system-prompt suffix at all, so every referral shipped into
the harness suffix chain stopped at the process that refuses org turns. The
model reached the end of an answer, found it had no credential tool and no
place to name, and users pasted secrets rather than get nothing.

The suffix names the chat-side Connectors entry and nothing else. It names no
connector, because which ones a deployment offers comes from auth's OAuth
configuration that the pod cannot see, and claiming one either way risks
sending the user after something absent. It offers no other surface as a
workaround: this turn's connections are only what was listed in the oauth
block, so a credential saved elsewhere is unreachable from here.

The allowlist assertion compares against a literal set rather than the
constant, which the neighbouring test does and so never trips on an addition.
Any new tool now fails it, which is the point: a credential-capture tool would
make the referral untrue, and any other is a new place a pasted value can land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shared description tail still said to ask for missing pieces in a later
turn, so both tool variants carried a solicitation after the head text stopped
carrying one. The scrub-marker note still said to pass the actual secret values
instead, in the same tool result that now says not to ask for them; the values
behind a marker cannot be resolved from the vault at that point, so it now says
that and points at the credential flow.

The base prompt claimed the flow is named elsewhere in the prompt. On desktop it
is not: the harness attaches its workflow text to a skill bundle, so it reaches
the model as a tool rather than as prompt text. It now says either.

Both description tests asserted the single literal that had been deleted while
naming a property they did not check, which is how the tail survived. They now
scan every ask-shaped sentence and require it to be negated, which catches a
reworded solicitation and catches the tail. Negation strips apostrophes first,
since a word boundary never fires inside "don't".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rity

The suffix named a Connectors entry beside the chat. cowork's sidebar calls it
Connect Apps and Data, and reads Connected Apps and Data once something is
connected; Connectors titles a different page whose web branch saves personal
tokens through a route with no org-mode handling, into a vault no turn reads.
Sending a user there would have reproduced this dead end one step later, while
the suffix's own closing sentence denied it.

The suffix also refused to name another surface at all, which the ticket asks
for the opposite of: state parity plainly, either it works here or say where it
must happen and how. The web UI already points at the desktop app in two
places, so saying nothing had the agent contradict the screen. It now says it,
with the part the UI leaves out: a credential added there is not readable from
this conversation, so the work moves rather than the credential.

The test pinned the absence of that referral, which made it a test of the
choice rather than of the requirement. It now pins the referral and its caveat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The base prompt and the connector tool descriptions are shared with the CLI and
the desktop, and this ticket is about web users, so they go back to what they
were. Only the cloud-turn suffix remains.

The suffix now carries the whole rule rather than half of it. It has to: with
the shared prompt reverted, nothing else on this surface forbids asking for a
credential, tells the user to rotate one that arrives anyway, or forbids
storing it. That last clause is the one that cannot be dropped, because
memorize and create_skill_draft are both allowlisted here, memory writes are
applied org-side and replayed on later turns, and the relay scrubber matches
four shapes that a GitHub PAT, a WordPress application password and an SMTP
password all miss.

Two costs, both deliberate. The shared prompt still lists credentials among the
things to ask the user for, so the suffix countermands it in words rather than
relying only on being appended last. And the solicitation on the desktop path
stays as it is, though five users pasted live credentials there in a single
week, so it wants its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b1e1845af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread anton/cloud_turn/session.py Outdated
# line is derived by the session from the provider's response.
system_prompt_context=SystemPromptContext(
runtime_context=build_runtime_context(settings),
suffix=_CREDENTIAL_CONTEXT,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply the credential policy before direct router replies

When ANTON_ROUTER_ENABLED=true, text turns first go through gate_turn, whose independent _THALAMUS_SYSTEM_PROMPT never receives this SystemPromptContext; an ACTION_RESPOND result is returned directly without invoking the planning model. A vague setup request that the router answers with a clarifying question—or a pasted credential that the existing scrubber does not recognize—can therefore still cause the assistant to solicit or repeat a secret despite this new policy. Pass the credential policy to the gate or prevent direct routing for these cloud turns.

Useful? React with 👍 / 👎.

Comment thread anton/cloud_turn/session.py Outdated
# line is derived by the session from the provider's response.
system_prompt_context=SystemPromptContext(
runtime_context=build_runtime_context(settings),
suffix=_CREDENTIAL_CONTEXT,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Place the credential policy after remembered rules

When a turn has org or project memory, ChatSystemPromptBuilder.build appends memory_context after this suffix (prompt_builder.py:213-216), contrary to the placement assumption above. A remembered Always rule instructing the agent to request or store credentials consequently appears later than the safety policy, while the policy only explicitly overrides “earlier” instructions; this can preserve the credential-leak behavior for affected conversations. Ensure this guard is rendered after the volatile memory tail, or otherwise give it precedence over remembered rules.

Useful? React with 👍 / 👎.

tino097 and others added 4 commits September 11, 2026 12:07
… came before

The remembered-rules snapshot renders after the suffix, so a rule asking for
an API key was never countermanded by a guard that claimed precedence only
over earlier instructions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the placement note

A recalled skill body is the organization's own text and arrives as tool
output, so a guard bounded by this prompt did not reach it. The comment
claiming the suffix renders last was disproved by the commit before this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Artifact delivery and the credential rule both set the single suffix field.
They are joined into one constant so the whole of what this surface says can
be pinned by a test, and neither block depends on its position.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ents it

The builder's volatile tail is not a cached boundary: anton places no
cache_control and the gateway marks the whole system string as one block, so
where the guard sits inside it has no cache consequence.

Co-Authored-By: Claude Opus 5 (1M context) <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.

1 participant