Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/scripts/check_persisted_settings_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ def emit(key: str, payload: dict[str, Any]) -> None:
acp = ACPAgentSettingsCls(
acp_server="claude-code",
acp_model="claude-opus-4-6",
acp_env={"OPENAI_API_KEY": "sk-test-acp"},
)
except Exception:
acp = None
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/run-eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ on:
sdk_ref:
description: SDK commit/ref to evaluate (must be a semantic version like v1.0.0 unless 'Allow unreleased branches' is checked)
required: true
default: v1.28.0
default: v1.29.0




Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ When reviewing code, provide constructive feedback:
- Remote workspace git operations should call `/api/git/changes` and `/api/git/diff` via the `path` query parameter with slash-normalized strings; building those URLs with `pathlib.Path` leaks host-platform separators and breaks Windows paths. The grep tool now prefers `rg`, then system `grep`, then Python; both the real grep executor and the SDK's terminal-command compatibility fallback should keep that order. For grep parity, the Python fallback should hide dotfiles by default but still let explicit `include` globs surface files like `.env`, matching ripgrep. For glob parity, any symlink-preservation regression test should force the Python fallback path, because ripgrep availability changes whether the fallback implementation runs at all.
- Keep path helpers split by purpose: `is_absolute_path_source()` is for cross-platform source/wire syntax detection, while local filesystem writes/validation (for example, the file editor) should use host-native absolute-path semantics so POSIX does not silently accept Windows drive paths as creatable files.
- Tool availability filtering belongs in `openhands-sdk/openhands/sdk/tool/registry.py` via `list_usable_tools()`, which preserves registration order and defaults tools to usable unless they expose an `is_usable()` callable. Environment-specific checks like Chromium detection should live on the concrete tool class (`BrowserToolSet.is_usable()`), while agent-server surfaces such as `/server_info` should consume the registry helper rather than re-implement per-tool filtering.
- Pydantic secret field helpers live in `openhands-sdk/openhands/sdk/utils/pydantic_secrets.py`. `serialize_secret()` handles serialization (cipher / `expose_secrets` / default Pydantic masking); `validate_secret()` handles deserialization (cipher decryption, redacted/empty → `None`); `is_redacted_secret()` checks for the sentinel; `REDACTED_SECRET_VALUE` is the canonical sentinel string. For `dict[str, str]` fields whose values are all secrets, wrap each value in `SecretStr` and call `serialize_secret` per value (see `LookupSecret._serialize_secrets` and `ACPAgent._serialize_acp_env`). Do not hand-roll redaction logic in field serializers.
- Pydantic secret field helpers live in `openhands-sdk/openhands/sdk/utils/pydantic_secrets.py`. `serialize_secret()` handles serialization (cipher / `expose_secrets` / default Pydantic masking); `validate_secret()` handles deserialization (cipher decryption, redacted/empty → `None`); `is_redacted_secret()` checks for the sentinel; `REDACTED_SECRET_VALUE` is the canonical sentinel string. For `dict[str, str]` fields whose values are all secrets, wrap each value in `SecretStr` and call `serialize_secret` per value (see `LookupSecret._serialize_secrets`). Do not hand-roll redaction logic in field serializers.

- `LookupSecret` normalizes hostless URLs against `OH_INTERNAL_SERVER_URL` (set by `openhands-agent-server.__main__` from the bound host/port, rewriting wildcard binds to loopback) and otherwise falls back to `http://127.0.0.1:8000`, so relative secret URLs can safely target the current agent-server instance.

Expand Down
5 changes: 3 additions & 2 deletions examples/02_remote_agent_server/16_deferred_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,15 @@
assert resp.status_code == 200
data = resp.json()
execution_status = data.get("execution_status", "unknown")
if execution_status in ("stopped", "paused", "error"):
# Terminal states per ConversationExecutionStatus.is_terminal().
if execution_status in ("finished", "error", "stuck"):
break
logger.info(f" status: {execution_status} ({elapsed}s elapsed)")
time.sleep(2)
elapsed += 2

logger.info(f"✅ Conversation finished — status: {execution_status}")
assert execution_status in ("stopped", "paused"), (
assert execution_status == "finished", (
f"Unexpected final status: {execution_status}"
)

Expand Down
4 changes: 2 additions & 2 deletions openhands-agent-server/openhands/agent_server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ def _sanitize_validation_errors(errors: Sequence[Any]) -> list[dict]:
FastAPI's default 422 response includes the raw request ``input`` in each
validation error dict. If the request contained secret-bearing fields
(e.g. ``agent.llm.api_key``, ``agent.acp_env``), those values would be
(e.g. ``agent.llm.api_key``, MCP server ``env``), those values would be
echoed back to the caller. This helper redacts them.
Args:
Expand Down Expand Up @@ -457,7 +457,7 @@ async def _validation_exception_handler(
FastAPI's default 422 handler echoes the raw request body inside the
``detail[].input`` field. When the request contains secrets (e.g.
``agent.llm.api_key``, ``agent.acp_env``), this would leak credentials
``agent.llm.api_key``, MCP server ``env``), this would leak credentials
in the error response. We intercept the error, redact secret-bearing
fields, and return a safe 422 response.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ def _deep_merge(
- Nested dicts are merged recursively.
- **Inside a nested map** a ``None`` value **removes** that key — the
"unset" primitive a plain deep-merge lacks. It lets a
``PATCH /api/settings`` diff delete a single map entry (one
``acp_env`` / MCP ``env`` key) without round-tripping the whole map::
``PATCH /api/settings`` diff delete a single map entry (one MCP
``env`` / ``headers`` key) without round-tripping the whole map::

{"agent_settings_diff": {"acp_env": {"STALE_KEY": null}}}
{"agent_settings_diff":
{"mcp_config": {"mcpServers": {"svc": {"env": {"STALE_KEY": null}}}}}}

- **At the top level** (a settings *field* like ``confirmation_mode`` or
``acp_env`` itself) a ``None`` is left as-is and flows to model
- **At the top level** (a settings *field* like ``confirmation_mode``)
a ``None`` is left as-is and flows to model
validation — exactly as before this primitive existed. So a stray
``{"confirmation_mode": null}`` still fails loudly (422) instead of
silently resetting a field to its default. This scoping is deliberate:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,9 @@ async def update_settings(
The three ``*_settings_diff`` fields are deep-merged; nested objects merge
recursively, and a ``null`` value **inside a nested map deletes that entry**
— the "unset" primitive that lets a client remove a single map key without
round-tripping the whole map. To drop one ACP env-var::
round-tripping the whole map. To remove one MCP server's header::
PATCH /api/settings
{"agent_settings_diff": {"acp_env": {"STALE_KEY": null}}}
or to remove one MCP server's header::
{"agent_settings_diff":
{"mcp_config": {"mcpServers": {"svc": {"headers": {"X-Old": null}}}}}}
Expand Down
2 changes: 1 addition & 1 deletion openhands-agent-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "openhands-agent-server"
version = "1.28.0"
version = "1.29.0"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"

requires-python = ">=3.12"
Expand Down
Loading
Loading