Skip to content

feat(cloud): harden workspace shell parity - #104

Merged
delete-cloud merged 5 commits into
mainfrom
cloud-workspace-pr4-lifecycle-parity
May 4, 2026
Merged

delete-cloud merged 5 commits into
mainfrom
cloud-workspace-pr4-lifecycle-parity

Conversation

@delete-cloud

@delete-cloud delete-cloud commented May 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • preserve cloud shell session semantics for cd and export, surface deterministic timeout failures, and reject explicit cwd escapes before remote execution
  • tag cloud-backed runs with authoritative cloud.workspace_id trace metadata without leaking workspace URLs or trusting caller-supplied cloud markers
  • add focused parity regressions plus rerun the ADR17 cloud workspace acceptance slice

Verification

  • uv run pytest tests/coding_agent/environment/test_cloud_environment.py tests/coding_agent/plugins/test_core_tools.py tests/coding_agent/test_bootstrap.py -k \"cloud or non_local or workspace\" -v
  • uv run pytest tests/agentkit/environment/ tests/coding_agent/environment/ tests/coding_agent/plugins/test_core_tools.py tests/coding_agent/test_bootstrap.py tests/ui/test_execution_binding.py tests/ui/test_session_manager_runtime.py tests/ui/test_http_server.py -k \"environment or cloud or binding or workspace\" -v
  • uv run pytest tests/coding_agent/tools/test_shell.py tests/tools/test_shell.py -k \"cd or export or timeout or non_zero_exit_code\" -v
  • uv run python -m compileall -q src/agentkit src/coding_agent tests/agentkit/environment tests/coding_agent tests/ui
  • manual driver: exercised cloud cd, export, timeout, cwd escape rejection, and trace metadata behavior

Notes

  • uv run ruff format ... is still unavailable here because ruff is not installed/spawnable in this environment

Summary by CodeRabbit

  • New Features

    • Cloud shell now supports cd and export with workspace-aware path validation and clear error/timeout messages.
    • Trace metadata is normalized for cloud environments and sanitized for local runs, ensuring workspace IDs are set or cloud keys removed.
  • Tests

    • Expanded coverage for cloud shell behavior, timeout visibility, and trace metadata handling.
  • Refactor

    • Internal session/environment resolution simplified (no user-facing behavior changes).

delete-cloud and others added 2 commits May 4, 2026 20:57
Align cloud shell behavior with local session semantics by handling cd/export in-session, surfacing deterministic timeout errors, and rejecting explicit cwd escapes before remote execution.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Expose an authoritative cloud workspace id in run trace metadata so runtime context stays useful without leaking workspace URLs or trusting caller-supplied cloud markers.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR adds a _run_trace_metadata helper that normalizes and injects cloud workspace IDs into agent run trace metadata, updates agent creation to use it, and enhances cloud shell tooling to parse cd/export, validate/resolve cwd inside the cloud workspace, and surface validation/timeout errors; tests and some session/environment wiring were updated accordingly.

Changes

Trace metadata normalization

Layer / File(s) Summary
New helper
coding-agent/src/coding_agent/app.py
Adds _run_trace_metadata(environment, trace_metadata) which copies incoming mapping, strips existing cloud.* keys for cloud environments, validates workspace_id from environment.tool_config() is a non-empty string, and sets cloud.workspace_id.
Wiring
coding-agent/src/coding_agent/app.py
create_child_pipeline now calls _run_trace_metadata(environment, trace_metadata) and passes its result into AgentRunContext.trace_metadata rather than passing raw mapping or {}.

Cloud shell tooling, tests, and session wiring

Layer / File(s) Summary
Imports / types
coding-agent/src/coding_agent/environment/cloud.py
Adds posixpath and shlex imports and a typed client attribute on CloudEnvironment (self._client: CloudWorkspaceClient).
Core helpers
coding-agent/src/coding_agent/environment/cloud.py
Adds helpers to resolve/normalize target paths relative to workspace root, validate an explicit cwd to ensure it remains inside the workspace, and parse standalone cd <dir> and export KEY=VALUE forms (raising ValueError on invalid forms).
Shell tool behavior
coding-agent/src/coding_agent/environment/cloud.py
build_shell_tool's bash_run now intercepts cd/export-only commands and returns formatted messages, validates/normalizes cwd into execution_cwd before calling run_command, and returns user-visible Error: ... strings for ValueError and timeout-specific errors for TimeoutError.
Tests
coding-agent/tests/coding_agent/environment/test_cloud_environment.py, coding-agent/tests/coding_agent/test_bootstrap.py
Adds tests verifying cd/export session updates, rejection of invalid cd/export without session mutation, explicit cwd boundary enforcement, timeout visibility; extends FakeCloudWorkspaceClient with command_timeout to raise TimeoutError; adds bootstrap tests validating cloud trace metadata normalization strips secrets and sets workspace_id.
Session/environment wiring
coding-agent/src/coding_agent/ui/session_manager.py, coding-agent/src/coding_agent/ui/binding_resolver.py
SessionManager now resolves Environment and workspace root via _resolve_environment(session) and _environment_workspace_root(environment) at call sites (removed internal helper methods). BindingResolver.resolve_workspace_root protocol method declaration changed to an ellipsis stub.

Sequence Diagram

sequenceDiagram
    participant User
    participant App as Agent Creation<br/>app.py
    participant Trace as _run_trace_metadata
    participant Env as CloudEnvironment<br/>(cloud.py)
    participant Shell as Shell Tool<br/>bash_run
    participant Client as CloudWorkspaceClient

    User->>App: create_agent(trace_metadata)
    App->>Trace: _run_trace_metadata(environment, trace_metadata)
    Trace->>Env: is environment.kind == "cloud"?
    alt cloud
        Trace->>Env: environment.tool_config()
        Env-->>Trace: workspace_id
        Trace-->>App: sanitized trace_metadata (cloud.workspace_id set, cloud.* removed)
    else non-cloud
        Trace-->>App: trace_metadata (unchanged)
    end

    User->>Shell: execute "cd /path" or "export KEY=VAL" (with cwd/env)
    Shell->>Shell: parse command via shlex
    Shell->>Shell: resolve/validate execution_cwd with posixpath (ensure inside workspace)
    alt valid and parsed cd/export
        Shell-->>User: "Changed directory ..." or "Exported KEY=VALUE"
    else validated execution -> run
        Shell->>Client: run_command(cmd, cwd=execution_cwd, env=...)
        Client-->>Shell: result or TimeoutError
        alt TimeoutError
            Shell-->>User: "Error: cloud command timed out (timed out after Xs)"
        else
            Shell-->>User: output
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I tidy traces, strip secrets away,
paths hop safely where cloud roots stay.
cd and export now speak polite,
timeouts shown in plain daylight—
a rabbit bounds, content to play. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "feat(cloud): harden workspace shell parity" is closely related to the main changes, particularly the shell hardening work for cloud environments including cd/export handling and timeout failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cloud-workspace-pr4-lifecycle-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

delete-cloud and others added 3 commits May 4, 2026 22:44
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (4)
coding-agent/tests/coding_agent/environment/test_cloud_environment.py (2)

236-245: ⚡ Quick win

Test name implies prefix-collision test but covers an unrelated case; the /workspacefoo prefix attack is untested.

test_cloud_environment_rejects_cd_and_explicit_cwd_with_same_prefix tests cd /etc and cwd="/etc", which are unambiguously outside the workspace. The prefix-collision case the name refers to (a path like /workspacefoo sharing the /workspace prefix but not a child) is handled by workspace_root.rstrip("/") + "/" in _resolve_workspace_path but never exercised.

💡 Add the actual prefix-attack case
 def test_cloud_environment_rejects_cd_and_explicit_cwd_with_same_prefix() -> None:
     client = FakeCloudWorkspaceClient()
     shell_tool = CloudEnvironment(client).build_shell_tool()

-    cd_result = shell_tool("cd /etc", cwd="/workspace")
-    cwd_result = shell_tool("pwd", cwd="/etc")
+    # /workspacefoo shares the /workspace prefix but is NOT a child directory
+    cd_result = shell_tool("cd /workspacefoo", cwd="/workspace")
+    cwd_result = shell_tool("pwd", cwd="/workspacefoo")

-    assert cd_result == "Error: Directory is outside cloud workspace: /etc"
-    assert cwd_result == "Error: Directory is outside cloud workspace: /etc"
+    assert cd_result == "Error: Directory is outside cloud workspace: /workspacefoo"
+    assert cwd_result == "Error: Directory is outside cloud workspace: /workspacefoo"
     assert client.shell_calls == []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coding-agent/tests/coding_agent/environment/test_cloud_environment.py` around
lines 236 - 245, Update the test
test_cloud_environment_rejects_cd_and_explicit_cwd_with_same_prefix to exercise
the prefix-collision case by invoking the shell tool with a path that shares the
workspace prefix but is not a child (e.g. "/workspacefoo") so
_resolve_workspace_path's workspace_root.rstrip("/") + "/" logic is exercised;
use FakeCloudWorkspaceClient and CloudEnvironment().build_shell_tool() the same
way, call shell_tool("cd /workspacefoo", cwd="/workspacefoo") (and/or
shell_tool("pwd", cwd="/workspacefoo")) and assert it returns the same "Error:
Directory is outside cloud workspace: /workspacefoo" and that client.shell_calls
remains empty.

126-134: 💤 Low value

Two separate CloudEnvironment(client) instances may cause confusion; consider sharing one.

shell_session.do_mount(...) receives the first CloudEnvironment(client), and CoreToolsPlugin(...) receives a second independent instance. Since CloudEnvironment is stateless (all state lives in client), this works correctly today — but if CloudEnvironment ever gains instance state, the two instances would diverge silently.

💡 Suggested simplification
+    env = CloudEnvironment(client)
     shell_session = ShellSessionPlugin()
     _ = shell_session.do_mount(
-        ctx=type("Ctx", (), {"config": {"environment": CloudEnvironment(client)}})()
+        ctx=type("Ctx", (), {"config": {"environment": env}})()
     )
     plugin = CoreToolsPlugin(
-        environment=CloudEnvironment(client),
+        environment=env,
         shell_session=shell_session,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coding-agent/tests/coding_agent/environment/test_cloud_environment.py` around
lines 126 - 134, The test creates two separate CloudEnvironment(client)
instances (one passed into shell_session.do_mount via the fake ctx and a second
passed to CoreToolsPlugin), which could diverge if CloudEnvironment later
becomes stateful; change the test to instantiate a single CloudEnvironment once
(e.g., env = CloudEnvironment(client)) and pass that same env into the ctx
passed to ShellSessionPlugin.do_mount and into CoreToolsPlugin to ensure both
shell_session.do_mount and CoreToolsPlugin use the identical CloudEnvironment
instance; keep references to FakeCloudWorkspaceClient,
ShellSessionPlugin.do_mount, CoreToolsPlugin, CloudEnvironment, shell_session
and plugin to locate and update the code.
coding-agent/tests/coding_agent/test_bootstrap.py (1)

332-396: 💤 Low value

New trace-metadata tests are thorough; the string-scan assertion is slightly fragile.

Both tests correctly exercise _run_trace_metadata: the cloud test confirms stale cloud.workspace_id is overwritten and workspace_url is excluded; the local test confirms all cloud.* keys are stripped. One minor fragility: the str(dict(…)) secret-scan on lines 364 and 396 would false-positive if any legitimate metadata value happened to contain the substring "secret" or "cloud.". The direct key assertions on lines 363 and 394 already provide the meaningful coverage; consider replacing the string-scan assertions with more precise checks.

💡 More precise alternative
-        assert "secret" not in str(dict(ctx.run_context.trace_metadata))
+        assert not any("secret" in str(v) for v in ctx.run_context.trace_metadata.values())
-        assert "cloud." not in str(dict(ctx.run_context.trace_metadata))
+        assert not any(k.startswith("cloud.") for k in ctx.run_context.trace_metadata)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coding-agent/tests/coding_agent/test_bootstrap.py` around lines 332 - 396,
Replace the fragile string-scans in the two tests
(test_create_agent_adds_secret_free_cloud_workspace_trace_metadata and
test_create_agent_strips_cloud_trace_metadata_for_local_environment) with
precise checks against the trace_metadata dict: assert directly that no key
startswith "cloud." (e.g., all(not k.startswith("cloud.") for k in
ctx.run_context.trace_metadata.keys())) and assert that no value contains the
substring "secret" (e.g., all("secret" not in str(v) for v in
ctx.run_context.trace_metadata.values())); keep the existing direct key
assertions (like exact trace_metadata equality and absence/presence of
workspace_url) and remove the str(dict(...)) assertions.
coding-agent/src/coding_agent/environment/cloud.py (1)

153-191: 💤 Low value

bash_run return-message format for cd/export is an implicit contract with ShellSessionPlugin.

When _apply_cd or _apply_export succeeds, bash_run returns a plain string ("Changed directory to {path}" / "Exported {key}={value}"). ShellSessionPlugin presumably parses these exact strings to update session cwd/env_vars. This creates a silent integration contract: changing the message format (e.g., internationalizing it, adding punctuation) would break session state tracking without any compile-time warning.

Consider documenting this contract with a constant or structured return, or verify it is explicitly covered by the integration test in test_cloud_environment_shell_tool_updates_session_cwd_and_env.

#!/bin/bash
# Verify how ShellSessionPlugin processes bash_run return values for cd/export
rg -n "Changed directory\|Exported\|cd\|export\|env_vars\|session_cwd" \
  --type py -A3 -B3 \
  coding-agent/src/coding_agent/plugins/shell_session.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coding-agent/src/coding_agent/environment/cloud.py` around lines 153 - 191,
The bash_run function in build_shell_tool currently returns free-form strings
for _apply_cd and _apply_export ("Changed directory to ..." / "Exported ..."),
creating an implicit contract with ShellSessionPlugin; change bash_run to return
a structured, stable indicator (e.g., a constant tag plus payload or a dict-like
string) instead of free-form text and update ShellSessionPlugin and the test
test_cloud_environment_shell_tool_updates_session_cwd_and_env to parse that
structured response; specifically, introduce stable tokens or keys (e.g.,
"SHELL_SESSION_ACTION:cd:<path>" and "SHELL_SESSION_ACTION:export:<key>=<value>"
or JSON with action/path) in build_shell_tool::bash_run, update
ShellSessionPlugin to detect and handle those tokens (instead of exact
human-readable phrases), and ensure the integration test asserts the new
structured format so future formatting changes won’t break session cwd/env
updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@coding-agent/src/coding_agent/environment/cloud.py`:
- Around line 153-191: The bash_run function in build_shell_tool currently
returns free-form strings for _apply_cd and _apply_export ("Changed directory to
..." / "Exported ..."), creating an implicit contract with ShellSessionPlugin;
change bash_run to return a structured, stable indicator (e.g., a constant tag
plus payload or a dict-like string) instead of free-form text and update
ShellSessionPlugin and the test
test_cloud_environment_shell_tool_updates_session_cwd_and_env to parse that
structured response; specifically, introduce stable tokens or keys (e.g.,
"SHELL_SESSION_ACTION:cd:<path>" and "SHELL_SESSION_ACTION:export:<key>=<value>"
or JSON with action/path) in build_shell_tool::bash_run, update
ShellSessionPlugin to detect and handle those tokens (instead of exact
human-readable phrases), and ensure the integration test asserts the new
structured format so future formatting changes won’t break session cwd/env
updates.

In `@coding-agent/tests/coding_agent/environment/test_cloud_environment.py`:
- Around line 236-245: Update the test
test_cloud_environment_rejects_cd_and_explicit_cwd_with_same_prefix to exercise
the prefix-collision case by invoking the shell tool with a path that shares the
workspace prefix but is not a child (e.g. "/workspacefoo") so
_resolve_workspace_path's workspace_root.rstrip("/") + "/" logic is exercised;
use FakeCloudWorkspaceClient and CloudEnvironment().build_shell_tool() the same
way, call shell_tool("cd /workspacefoo", cwd="/workspacefoo") (and/or
shell_tool("pwd", cwd="/workspacefoo")) and assert it returns the same "Error:
Directory is outside cloud workspace: /workspacefoo" and that client.shell_calls
remains empty.
- Around line 126-134: The test creates two separate CloudEnvironment(client)
instances (one passed into shell_session.do_mount via the fake ctx and a second
passed to CoreToolsPlugin), which could diverge if CloudEnvironment later
becomes stateful; change the test to instantiate a single CloudEnvironment once
(e.g., env = CloudEnvironment(client)) and pass that same env into the ctx
passed to ShellSessionPlugin.do_mount and into CoreToolsPlugin to ensure both
shell_session.do_mount and CoreToolsPlugin use the identical CloudEnvironment
instance; keep references to FakeCloudWorkspaceClient,
ShellSessionPlugin.do_mount, CoreToolsPlugin, CloudEnvironment, shell_session
and plugin to locate and update the code.

In `@coding-agent/tests/coding_agent/test_bootstrap.py`:
- Around line 332-396: Replace the fragile string-scans in the two tests
(test_create_agent_adds_secret_free_cloud_workspace_trace_metadata and
test_create_agent_strips_cloud_trace_metadata_for_local_environment) with
precise checks against the trace_metadata dict: assert directly that no key
startswith "cloud." (e.g., all(not k.startswith("cloud.") for k in
ctx.run_context.trace_metadata.keys())) and assert that no value contains the
substring "secret" (e.g., all("secret" not in str(v) for v in
ctx.run_context.trace_metadata.values())); keep the existing direct key
assertions (like exact trace_metadata equality and absence/presence of
workspace_url) and remove the str(dict(...)) assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 36b5a2e8-b40a-4770-8daf-33ef8e643af3

📥 Commits

Reviewing files that changed from the base of the PR and between 33dfb01 and 58ba796.

📒 Files selected for processing (6)
  • coding-agent/src/coding_agent/app.py
  • coding-agent/src/coding_agent/environment/cloud.py
  • coding-agent/src/coding_agent/ui/binding_resolver.py
  • coding-agent/src/coding_agent/ui/session_manager.py
  • coding-agent/tests/coding_agent/environment/test_cloud_environment.py
  • coding-agent/tests/coding_agent/test_bootstrap.py
💤 Files with no reviewable changes (1)
  • coding-agent/src/coding_agent/ui/binding_resolver.py

@delete-cloud
delete-cloud merged commit 350c977 into main May 4, 2026
1 check passed
@delete-cloud
delete-cloud deleted the cloud-workspace-pr4-lifecycle-parity branch May 4, 2026 15:11
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