Skip to content

feat: agent secret scope#285

Open
mlikasam-askui wants to merge 2 commits into
mainfrom
feat/secrets-scope
Open

feat: agent secret scope#285
mlikasam-askui wants to merge 2 commits into
mainfrom
feat/secrets-scope

Conversation

@mlikasam-askui

@mlikasam-askui mlikasam-askui commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Lets users define secrets the agent can use (e.g. type a password) without ever exposing the value to the LLM.

The model only sees the placeholder <|secret|>NAME<|secret|>; the real value (held as a pydantic SecretStr) is substituted into tool inputs only at execution time. Literal values are redacted from the LLM conversation history and tool outputs, and the available placeholders are advertised to the model via an <AVAILABLE_SECRETS> system-prompt section.

Usage

from askui import ComputerAgent, Secret

with ComputerAgent(secrets=[Secret(name="password", value="...")]) as agent:
    agent.act("Log in as admin using the password")

What's included

  • New Secret / SecretVault (exported from askui); value accepts a plain str or SecretStr.
  • secrets= on all agent types (ComputerAgent, AndroidAgent, WebAgent, WebTestingAgent, MultiDeviceAgent) + per-call override via act(..., secrets=[...]). Excluded from telemetry.
  • Generic substitution in ToolCollection (works for type, custom, and MCP tools) + tool-output redaction.
  • type tool descriptions document the placeholder; example in examples/secrets.py; unit + integration tests.

Known limitation

A secret typed into a visible field can still appear in screenshots sent to the model — on-screen secrets aren't masked.

Jira: SOLENG-295

Let users define secrets the agent can use but the LLM never sees. Secrets are
referenced by the placeholder `<|secret|>NAME<|secret|>`; the real value (held
as a pydantic SecretStr) is substituted into tool inputs only at execution time
and is kept out of the model. Literal values are redacted from the LLM
conversation history and tool outputs, and the available placeholders are
advertised to the model via an <AVAILABLE_SECRETS> system-prompt section.

- New Secret / SecretVault (models/shared/secrets.py), exported from `askui`;
  `value` accepts a plain str or SecretStr and is stored as SecretStr.
- ComputerAgent / AndroidAgent / Agent.act accept `secrets=[...]` (agent-level
  with per-call override); excluded from telemetry.
- ToolCollection substitutes placeholders in tool inputs (works for MCP and
  custom tools) and redacts secret values from tool outputs.
- Conversation redacts messages added to history, advertises placeholders, and
  deep-copies act settings so per-call prompt sections don't accumulate.
- `type` tool descriptions document placeholder usage; deterministic type()
  resolves placeholders and redacts its report line.
- Example (examples/secrets.py) plus unit and integration tests.

Jira: SOLENG-295
@mlikasam-askui mlikasam-askui self-assigned this Jun 17, 2026
@mlikasam-askui mlikasam-askui requested review from philipph-askui and programminx-askui and removed request for programminx-askui June 17, 2026 11:27

@philipph-askui philipph-askui left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please update the docs to reflect the new concept

  • I assume you tested this?

# not leak into the conversation history / model.
return ToolResultBlockParam(
content=_convert_to_content(tool_result),
content=self._secret_vault.redact_content(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what if the name of the secret is by concidence an important information. Could happen if the secret is e.g. adminstrator or similar. Than this would be misidentified as secret and redacted, no?

Comment on lines +42 to +43
_PLACEHOLDER_PREFIX = "<|secret|>"
_PLACEHOLDER_SUFFIX = "<|secret|>"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why this format and not standard xml format or just a bullet point list?

@philipph-askui philipph-askui left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR review from Claude below

PR Review: feat: agent secret scope

Overall Assessment

Well-designed feature with a clear trust model. The placeholder-based approach (<|secret|>NAME<|secret|>) is a solid choice — it keeps secrets out of the LLM context while remaining readable. The code is well-documented, follows the project's conventions, and the unit tests are thorough. A few issues to address below.


Critical

1. _redact_block() mutates its argument in-place (secrets.py:228-235)

redact_message() does message.model_copy(deep=True) which is correct, but _redact_block() mutates the block objects directly (block.text = ..., block.content = ..., block.input = ...). Since model_copy(deep=True) creates deep copies this happens to work, but it's fragile — if _redact_block is ever called outside of redact_message (e.g. via redact_content, which IS used independently in tools.py), it will mutate the original content blocks.

In ToolCollection._run_regular_tool() and _run_mcp_tool(), redact_content() is called on freshly-constructed _convert_to_content() results, so it's safe today — but this is a latent foot-gun. Consider making _redact_block() return a copy consistently, or add a comment documenting the mutation contract.

2. Integration tests were deleted in commit 2

The integration tests (tests/integration/test_secrets.py, 212 lines) were created in commit 1 and then fully removed in commit 2. The final branch state has no integration tests for the secrets feature. Was this intentional? These tests (LLM never sees secret, reporter never sees secret, per-call override) would be valuable to keep.


Medium

3. __add__ on ToolCollection uses or for vault merging (tools.py)

secret_vault=other._secret_vault or self._secret_vault,

This picks other's vault if it's truthy (non-empty), otherwise falls back to self's. But if both vaults have secrets, secrets from self are silently dropped instead of being merged. Should this be self._secret_vault.merge(other._secret_vault) to be consistent with how act() merges vaults?

4. Secret value logged in _redact_str warning message — double-check

The warning in _redact_str() correctly logs secret.name and secret.placeholder (never the value). Good. But the log message says "before it left the trusted boundary" — this warning fires every time redaction happens (every _add_message call, every tool output). This could be extremely noisy in practice if the model echoes back typed text. Consider downgrading to logger.debug after the first occurrence, or using a set to warn only once per secret per act() call.

5. _substitute_str tolerates whitespace but _redact_str doesn't

_substitute_str strips whitespace from placeholder names (name = match.group(1).strip()), which is nice. But _redact_str does exact substring matching of the literal value. This asymmetry seems intentional (redaction is defense-in-depth), but worth a brief doc comment to avoid confusion.


Minor / Nits

6. WebTestingAgent missing secrets in telemetry exclude

WebAgent.__init__ correctly adds "secrets" to the telemetry exclude set, but WebTestingAgent doesn't appear to — worth checking that secret values don't leak into telemetry there.

7. Stale docstring arg in act() (agent_base.py:154)

            act_model (ActModel | None, optional): Model to use for this act
                execution.

This act_model parameter doesn't exist in the signature anymore (looks like it was removed in a previous PR but the docstring wasn't updated). Pre-existing issue, but since you're touching this method it's a good time to clean it up.

8. Type annotation for _redact_block could be tighter

_redact_block(self, block: Any) -> Any — since it handles TextBlockParam | ToolResultBlockParam | ToolUseBlockParam, a ContentBlockParam union type would be more precise.

9. Example file uses hardcoded secret value

examples/secrets.py includes "hunter2-demo-only" as a hardcoded value. The docstring warns this is for demo only, which is fine, but consider whether this is the right pattern to show in an example file that users will copy-paste. The env-var approach is already demonstrated — the hardcoded variant could be removed or at minimum moved to the end and de-emphasized.

10. Missing __init__.py for test directory?

Convention says "Create __init__.py in each folder" — tests/unit/test_secrets.py is a new file. If there isn't already an __init__.py in tests/unit/, one should be added. (Likely already exists, just flagging.)


What looks good

  • Clean separation of concerns: SecretVault owns all substitution/redaction logic, agents just call it.
  • Defense-in-depth: Redaction at multiple layers (goal, initial messages, _add_message, tool outputs) is well-thought-out.
  • Immutability: substitute() and redact() return new objects. redact_message() deep-copies. Good.
  • Deep-copy of ActSettings: The model_copy(deep=True) in _setup_control_loop fixes what would have been a nasty bug with accumulating system prompt sections across calls.
  • Pydantic SecretStr: Proper masking in repr/dump/json.
  • Comprehensive unit tests: 26 tests covering substitution, redaction, merging, system prompt generation, tool output redaction, and edge cases.
  • Consistent propagation: All agent types (Computer, Android, Web, WebTesting, MultiDevice) forward secrets correctly.
  • Placeholder design: Using <|secret|> delimiters is unlikely to collide with natural text.

act_tools: list[Tool] | None = None,
callbacks: list[ConversationCallback] | None = None,
truncation_strategy: TruncationStrategy | None = None,
secrets: list[Secret] | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you not instroduced a SecretVault? I want to have the possibilitiy to plugin different stores.

Comment thread examples/secrets.py
@@ -0,0 +1,100 @@
"""Example demonstrating the secret scope.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this not moved to a docs section?

Comment thread examples/secrets.py

Secrets let the agent *use* sensitive values (e.g. type a password) while the value is
never sent to the LLM. The model only ever sees the placeholder
``<|secret|>NAME<|secret|>``; the real value is substituted into tool calls at execution

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the <|secret|> pre- & suffix is to annyoing.

# Store execution parameters. Deep-copy settings so per-call mutations
# (speaker handoff + secret sections appended to the system prompt) do not
# accumulate on the Agent's persistent, reused settings object across calls.
self.settings = (settings or ActSettings()).model_copy(deep=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why a deep copy?

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.

3 participants