Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/features/credentials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ description: 'Extend Mobilerun with secure credential management'
## Overview

Secure storage for passwords, API keys, and tokens.
- Stored in YAML files or in-memory dicts
- Stored in YAML files, environment variables, or in-memory dicts
- Never logged or exposed
- Auto-injected as `type_secret` action
- Resolved in `{{SECRET_ID}}` placeholders at execution time
- Simple string or dict format

## Quick Start
Expand Down Expand Up @@ -56,6 +57,11 @@ secrets:
value: "gmail_pass_123"
enabled: true

# Environment variable reference
CI_PASSWORD:
env: "MOBILERUN_CI_PASSWORD"
enabled: true

# Simple string format (auto-enabled)
API_KEY: "sk-1234567890abcdef"

Expand All @@ -72,6 +78,8 @@ secrets:
credentials:
enabled: true
file_path: config/credentials.yaml
env_keys: ["PASSWORD"] # optional: reads PASSWORD or MOBILERUN_PASSWORD
env_prefix: "MOBILERUN_" # optional
```

3. **Use in code:**
Expand Down Expand Up @@ -115,6 +123,21 @@ When credentials are provided, the `type_secret` action is **automatically avail

The agent never sees the actual value - only the secret ID.

### Placeholder Mode

Regular `type` and `type_text` actions can include credential placeholders:

```json
{
"action": "type",
"text": "{{EMAIL_USER}} / {{EMAIL_PASS}}",
"index": 5
}
```

Known credential placeholders are resolved immediately before tool execution.
Tool events and action logs keep the original placeholder text instead of the secret value.

---

## Example: Login Automation
Expand Down
9 changes: 8 additions & 1 deletion docs/sdk/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ from mobilerun import CredentialsConfig
CredentialsConfig(
enabled=True, # Enable credential manager
file_path="config/credentials.yaml", # Path to credentials file
env_keys=["PASSWORD"], # Optional environment variable names/secret IDs
env_prefix="MOBILERUN_", # Optional prefix for env-backed secrets
)
```

Expand Down Expand Up @@ -294,6 +296,7 @@ agent = MobileAgent(
}
)
# Agent can call type_secret("USERNAME", index) and type_secret("PASSWORD", index)
# Agent can also type "{{USERNAME}}" or "{{PASSWORD}}" in regular type/type_text actions.
```

### Config Format
Expand All @@ -303,7 +306,9 @@ from mobilerun import MobileConfig, CredentialsConfig
config = MobileConfig(
credentials=CredentialsConfig(
enabled=True,
file_path="config/credentials.yaml"
file_path="config/credentials.yaml",
env_keys=["PASSWORD"],
env_prefix="MOBILERUN_",
)
)

Expand Down Expand Up @@ -571,6 +576,8 @@ logging:
credentials:
enabled: false
file_path: config/credentials.yaml
env_keys: []
env_prefix: ""
```

---
Expand Down
8 changes: 6 additions & 2 deletions mobilerun/agent/tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Set

from mobilerun.agent.action_result import ActionResult
from mobilerun.credential_manager.placeholders import resolve_credential_placeholders

if TYPE_CHECKING:
from llama_index.core.workflow import Context as WorkflowContext
Expand Down Expand Up @@ -134,10 +135,13 @@ async def execute(

entry = self.tools[name]
try:
call_args = await resolve_credential_placeholders(
args, getattr(ctx, "credential_manager", None)
)
if inspect.iscoroutinefunction(entry.fn):
result = await entry.fn(**args, ctx=ctx)
result = await entry.fn(**call_args, ctx=ctx)
else:
result = entry.fn(**args, ctx=ctx)
result = entry.fn(**call_args, ctx=ctx)
except TypeError as e:
result = ActionResult(
success=False,
Expand Down
13 changes: 11 additions & 2 deletions mobilerun/agent/utils/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from mobilerun.agent.action_result import ActionResult
from mobilerun.agent.oneflows.app_starter_workflow import AppStarter
from mobilerun.credential_manager.placeholders import resolve_credential_placeholders

logger = logging.getLogger("mobilerun")

Expand Down Expand Up @@ -258,7 +259,11 @@ async def type_text(
) -> ActionResult:
"""Type text into an indexed element or the currently focused input."""
try:
original_text = text
pre_ui = await _macro_pre_ui(ctx)
text = await resolve_credential_placeholders(
text, getattr(ctx, "credential_manager", None)
)
if index is not None and index != -1:
x, y = ctx.ui.get_element_coords(index)
await ctx.driver.tap(x, y)
Expand All @@ -273,7 +278,7 @@ async def type_text(
if success:
_record_macro_action(
ctx,
{"action_type": "input_text", "text": text, "clear": clear},
{"action_type": "input_text", "text": original_text, "clear": clear},
pre_ui=pre_ui,
)
return ActionResult(
Expand All @@ -292,12 +297,16 @@ async def type_text_direct(
) -> ActionResult:
"""Type text into the currently focused input."""
try:
original_text = text
pre_ui = await _macro_pre_ui(ctx)
text = await resolve_credential_placeholders(
text, getattr(ctx, "credential_manager", None)
)
success = await ctx.driver.input_text(text, clear)
if success:
_record_macro_action(
ctx,
{"action_type": "input_text", "text": text, "clear": clear},
{"action_type": "input_text", "text": original_text, "clear": clear},
pre_ui=pre_ui,
)
return ActionResult(
Expand Down
15 changes: 14 additions & 1 deletion mobilerun/config/credentials_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ secrets:
value: "example_password"
enabled: true

# === Environment Variable Format ===
# Reads the secret value from the named environment variable at runtime

ENV_PASSWORD:
env: "MOBILERUN_ENV_PASSWORD"
enabled: true

# === Simple String Format ===
# Automatically enabled when loaded

Expand All @@ -43,11 +50,16 @@ secrets:
# credentials:
# enabled: true
# file_path: config/credentials.yaml
# env_keys: ["PASSWORD"] # optional: reads PASSWORD or env_prefix + PASSWORD
# env_prefix: "MOBILERUN_" # optional: also discovers MOBILERUN_* as secrets
#
# 2. The agent can use the type_secret tool:
# {"action": "type_secret", "secret_id": "MY_PASSWORD", "index": 5}
#
# 3. Or pass credentials programmatically:
# 3. The agent can use placeholders in regular type/type_text actions:
# {"action": "type", "text": "{{MY_PASSWORD}}", "index": 5}
#
# 4. Or pass credentials programmatically:
# agent = MobileAgent(
# goal="login to app",
# credentials={"PASSWORD": "secret123"}
Expand All @@ -59,6 +71,7 @@ secrets:
# - Error messages show secret IDs but never values
# - Access is logged: "🔑 Accessing secret: 'MY_PASSWORD'"
# - The type_secret tool types secrets without exposing them
# - Placeholder tool events keep "{{MY_PASSWORD}}" instead of the resolved value
# - CredentialManager validates and sanitizes all inputs

# === File Format Notes ===
Expand Down
1 change: 1 addition & 0 deletions mobilerun/config/prompts/executor/system.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ The following secret IDs are available for use with the type_secret action:
{% endfor %}

When the current subgoal requires typing a secret (password, API key, etc.), use `type_secret` with the appropriate secret_id instead of the regular `type` action.
You can also type ordinary text that contains secret placeholders like `{% raw %}{{MY_PASSWORD}}{% endraw %}`; placeholders are resolved only during tool execution and logs keep the placeholder, not the secret value.
{% endif %}

### Latest Action History ###
Expand Down
1 change: 1 addition & 0 deletions mobilerun/config/prompts/fast_agent/system.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The credential manager has the following secret IDs available for use with the `
{% endfor %}

Use `type_secret` to type these secrets into input fields without exposing their values.
You may also use placeholders like `{% raw %}{{MY_PASSWORD}}{% endraw %}` in `type` or `type_text` parameters; placeholders are resolved only during tool execution and logs keep the placeholder, not the secret value.
{% endif %}

## Response Format
Expand Down
1 change: 1 addition & 0 deletions mobilerun/config/prompts/manager/system.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ The executor has access to the following secret IDs via the type_secret custom a
{% endfor %}

You can include these secret IDs in your plan when the task requires entering sensitive information (passwords, API keys, etc.). The executor will use `type_secret(secret_id, index)` to type them securely without exposing values.
For mixed text, you may plan placeholders like `{% raw %}{{MY_PASSWORD}}{% endraw %}` inside `type` text; placeholders are resolved only during tool execution and logs keep the placeholder, not the secret value.
</available_secrets>
{% endif %}
{% if output_schema %}
Expand Down
4 changes: 4 additions & 0 deletions mobilerun/config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ credentials:
enabled: false
# Path to credentials file (see config/credentials_example.yaml for format)
file_path: config/credentials.yaml
# Optional environment variables to expose as credentials.
# With env_prefix set, PASSWORD reads from MOBILERUN_PASSWORD.
env_keys: []
env_prefix: ""

# === MCP (Model Context Protocol) Settings ===
# Connect to MCP servers to extend agent capabilities with external tools
Expand Down
2 changes: 2 additions & 0 deletions mobilerun/config_manager/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ class CredentialsConfig:

enabled: bool = False
file_path: str = "config/credentials.yaml"
env_keys: List[str] = field(default_factory=list)
env_prefix: str = ""


@dataclass
Expand Down
2 changes: 2 additions & 0 deletions mobilerun/credential_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
CredentialNotFoundError,
)
from mobilerun.credential_manager.file_credential_manager import FileCredentialManager
from mobilerun.credential_manager.placeholders import resolve_credential_placeholders

__all__ = [
"CredentialManager",
"CredentialNotFoundError",
"FileCredentialManager",
"resolve_credential_placeholders",
]
Loading