Skip to content

[TH-9491] fix: keep URL credentials in an agent's description out of what the kit sends and stores - #278

Merged
khushalsonawat merged 8 commits into
feat/TH-8804-source-subfolderfrom
feat/TH-9491-redact-prompt-excerpt
Nov 11, 2026
Merged

khushalsonawat merged 8 commits into
feat/TH-8804-source-subfolderfrom
feat/TH-9491-redact-prompt-excerpt

Conversation

@khushalsonawat

@khushalsonawat khushalsonawat commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Description

When the kit reads an agent, it writes the agent's prompt, rules, summary and notes in its own words. That text can hold a web address with a password or key in it, such as https://bot:hunter2@hooks.example/v1?key=sk-1. Until now it reached the platform as written: in the provision payload, the suite evals, the judge's instructions, the stored reading and the hosted builder's chat.

After this change, every web address in that text loses its username, password, query and fragment wherever the kit saves or sends it.

Stacked on #271: this branch merges #271's head 53f06233, so that commit shows here until #271 lands.

Type of Change

  • Bug fix (security: customer credentials leaving the sandbox)

Changes

  • providers/agent.py, _redact: now also cleans addresses inside prose. It is still one cleaner, not a second one.
    • Every :// starts an address, even after a digit, dot or dash (1.https://…, -https://…, ${SCHEME}://…).
    • An address runs to the next whitespace, so no guessed end inside a word can leave a key behind.
    • A field that is only an address is cleaned as before.
  • harness/contract.py, one writer for contract.json: every writer goes through it, and it cleans the free text first. That covers the reading, the builder, the rule and tool amendments, and the repair steps.
    • Each field is marked as free text, which is cleaned, or technical, which is kept as written.
    • A test fails if a field is unmarked.
    • A scan test fails if a module writes contract.json without the writer.
  • Send-time guards: for readings saved by older versions, the text is cleaned again just before each send. That covers:
    • _chosen_evals_and_prompt, the provision payload;
    • run/grade.py, the suite evals' agent_prompt and the grade calls;
    • hosted_entrypoint._agent_instructions, the judge;
    • the live run view;
    • emit_model_event;
    • cli._render's stdout.
  • harness/hosted_chat_entrypoint.py, the hosted builder's chat:
    • Cleaned events: tool_result, the artifact paths, the question prompt and its options, and every assistant_message, on success, interruption and failure.
    • The model's text first: it is cleaned alone, before the save note, health lines and failure detail are added.
    • Streamed deltas: they go through a bounded holdback:
      • text since the last whitespace is held;
      • past 4,096 characters, everything before the last address start is sent;
      • past 16,384 characters, the address becomes [redacted];
      • assistant_message and the end of the turn flush what is held.
    • Also cleaned: amend's rule echo and cli's authoring drain.

Known limits (each has an owner)

  • Traces: TH-9504.
  • Owned by TH-9500, the secret scanner:
    • bare keys and tokens in an address's path;
    • the agent's own files;
    • a password that contains whitespace;
    • an address broken by a line break or tab inside a sentence.
  • The builder's other pipes, which still send raw: the SDK transcript mirror (PlatformSessionStore.append), coordinator_state, the remember notes and request_adjustment. TH-9532.
  • The model's files inside the workspace checkpoint archive: .futureagi-adk.db, .claude/, the pending-question file and the kept notes. TH-9521.
  • Honest text that changes: described in the design page's "What can go wrong". For example, words joined to an address that has a query are lost, and hosts are lowercased. In every case more is removed, never less.
  • A key split across two chat deltas past the 4,096-character hold-back goes out in two halves, each cleaned on its own. No real key is that long without whitespace.

Testing

Each test file was run on its own on the final commit:

test file passed
test_builder_chat 179
test_contract_writer 43
test_providers_agent_build 120
test_hosted_entrypoint 145
test_scenario_source_provision_agent 28
test_agent_reading 106
test_judge 71
test_harness -k suite_eval 5
test_hosted_conversation 22
test_contract_tools_subfolder 175
test_agent_type_refusal 515
tests/providers 161
test_harness_sandbox_server 36, plus 2 failures

The 2 failures in test_harness_sandbox_server are the base's own github_commit_sha_required failures, and they fail the same way on the base.

After merging the base at cf3ebb44, the files that overlap it passed again:

  • test_contract_writer 43;
  • test_harness_architecture 55;
  • test_agent_type_refusal 651;
  • test_contract_tools_subfolder 180;
  • test_bundle_author_v2 323;
  • test_instrumentation_evidence 48;
  • test_builder_chat 179;
  • test_harness_credentials 80.

Planted bugs check that the tests catch what they claim. Each was a one-token edit, restored and confirmed by sha256:

  • Round 3's fix: 60 plants, 58 caught. Of the two survivors, one is a named limit of the scan, and the other is caught by its own named test.
  • The last four tests, added for the fix check's Mediums: 4 plants, all caught.

Two reviewers each re-ran the leak probes, and both found 0 leaks:

  • reviewer A over 509,000 adversarial messages, plus 1,200 full turns;
  • reviewer B with its own fuzz.

Review history:

  • Plan review: three rounds, then a replanned design (v5.1) whose review cleared.
  • Change review: three rounds, then a check of the fix only by two fresh reviewers, as ruled after round 3. Both were CLEAR, and their Mediums are fixed in this commit.

Related Issues

Refs: TH-9491

…efore the platform (TH-9491)

These fail until the fix lands.

Refs: TH-9491
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>

# Conflicts:
#	tests/providers/test_providers_agent_build.py
…hat the kit sends and stores (TH-9491)

The kit writes an agent's prompt, rules, summary and notes in its own words.
That text can hold a web address with a password or key in it, and until now
it reached the platform as written. After this change, every web address in
that text loses its username, password, query and fragment wherever the kit
saves or sends it.

- providers/agent.py: _redact also cleans addresses inside prose. Every ://
  starts an address, and an address runs to the next whitespace, so no guessed
  end can leave a key behind.
- contract.py: one writer for contract.json. Each description field is marked
  as free text, which is cleaned, or technical, which is kept as written. A
  test fails if a field is unmarked, or if a module writes contract.json
  without going through the writer.
- Send-time guards re-clean before each send, for descriptions saved by older
  versions: the provision payload (_chosen_evals_and_prompt), the suite evals'
  agent_prompt, the judge's instructions, the live run view, the grade calls,
  emit_model_event, and cli._render's stdout.
- hosted_chat_entrypoint.py: the hosted builder's chat events are cleaned,
  covering the streamed deltas, assistant_message, tool_result, artifact
  paths and the question prompt. Deltas go through a bounded holdback: text
  since the last whitespace is held; past 4,096 characters, everything before
  the last address start is sent; past 16,384, the address becomes [redacted].
  amend's rule echo and cli's authoring drain are cleaned too.

Known limits, each owned elsewhere:
- traces: TH-9504.
- bare keys, path tokens, file contents, whitespace inside a password, and an
  address broken by a line break or tab in a sentence: TH-9500.
- the builder's transcript mirror, coordinator_state, remember notes and
  request_adjustment: TH-9532.
- the model's files in the workspace checkpoint archive: TH-9521.

Reviewed in three plan rounds and three change rounds, followed by a
fix-only check by two fresh reviewers, as the Coordinator ruled after round
3. Both reviewers were CLEAR, and their Mediums are fixed here.

Refs: TH-9491
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
@khushalsonawat khushalsonawat self-assigned this Nov 10, 2026
TH-9491's redaction property tests in tests/providers/test_providers_agent_build.py
import hypothesis, which the clean-install job did not install, so collection
failed. Install it at the dev group's minimum, the same pin as pyproject.toml.

Refs: TH-9491
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>

@cdileep23 cdileep23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please check the comments.

Comment thread src/fi/alk/providers/agent.py Resolved
Comment thread src/fi/alk/providers/agent.py Resolved
Comment thread src/fi/alk/providers/agent.py Resolved
Comment thread src/fi/alk/providers/agent.py Resolved
Comment thread src/fi/alk/harness/contract.py Resolved
Comment thread src/fi/alk/harness/contract.py Resolved
… into TH-9491

- tools.py: _save_contract keeps the single writer (write_contract) and takes
  #268's new reply text, which names unconfirmed prompt lines.
- contract.py: #268's new fields, unconfirmed_prompt_from and model (ModelSource),
  are marked as kept as written: they hold source lines and the cited model name,
  read by the build. The test that fails on an unmarked field caught them.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
khushalsonawat added a commit that referenced this pull request Nov 11, 2026
…h its tools.py and contract.py resolution) into TH-9454

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…288) into TH-9491

- tests/test_harness_sandbox_server.py: both sides appended tests; kept both.
- test_contract_writer's allow-list gains #269's two contract.json copies:
  reading_result.document's replace_if_unchanged (the saved contract's own bytes
  plus the reading record) and seed_contract.use_seed's _write_whole (the
  platform's seed, as written).
- sandbox_server.py imports SecretResolutionError, which #284's resolver raises
  but the base never imports: seven sandbox-server tests fail with NameError on
  the base itself.

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
@khushalsonawat
khushalsonawat merged commit 8bb5d39 into feat/TH-8804-source-subfolder Nov 11, 2026
11 checks passed
@khushalsonawat
khushalsonawat deleted the feat/TH-9491-redact-prompt-excerpt branch November 11, 2026 06:27
khushalsonawat added a commit that referenced this pull request Nov 11, 2026
…ture too

A message can carry a tool's input dumped as a JSON string inside JSON; cleaned as text, a URL
with a query in it would take the rest of the string. Raised by Ash from #278's review.

TH-9504

Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
khushalsonawat added a commit that referenced this pull request Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants