Skip to content

feat: tighten runtime policy + transport guards v2#2636

Merged
graycyrus merged 7 commits into
tinyhumansai:mainfrom
oxoxDev:feat/runtime-policy-tightening-v2
May 26, 2026
Merged

feat: tighten runtime policy + transport guards v2#2636
graycyrus merged 7 commits into
tinyhumansai:mainfrom
oxoxDev:feat/runtime-policy-tightening-v2

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 25, 2026

Summary

  • Follow-up to feat: tighten runtime policy + transport guards #2331. Three additional tightening commits on the action-tool / sandbox / embedded-webview permission surface.
  • Action-tool autonomy: node_exec + npm_exec now honour the can_act() gate that every other action tool already enforces.
  • Sandbox policy: find -execdir / -okdir join the existing -exec / -ok block; inline env-var prefixes that mutate downstream subprocess execution (pager / editor / SSH / diff hooks + dynamic-loader overrides + shell-eval vars) are rejected at allowlist time.
  • Embedded-webview perms: displayCapture removed from the Browser.grantPermissions set for Meet + Slack so Chromium's gesture gate on getDisplayMedia stays in force.

Problem

The shipped sandbox + action-tool surface had three remaining gaps that the earlier transport tightening (#2331) did not cover:

  1. Autonomy bypass on script-runner tools. file_write, edit_file, apply_patch, curl, http_request, pushover, csv_export, and friends all short-circuit on AutonomyLevel::ReadOnly via self.security.can_act(). node_exec and npm_exec did not — they only consulted the per-hour rate limiter. An agent configured for read-only operation (e.g. a Slack / iMessage / Discord assistant where the expected guarantee is "observe only, never act") could still run arbitrary JavaScript via inline_code, and through it arbitrary OS commands. Single prompt-injected message in a watched channel = full code execution on the host.

  2. find arg blocklist incomplete. is_args_safe("find") blocked -exec and -ok. The semantically identical -execdir and -okdir (same per-match command execution, just with cwd set to the match's parent) were not blocked. find is in the default allowlist, so the gap was directly reachable.

  3. Inline env-var prefix bypass. skip_env_assignments() stripped leading KEY=value tokens before the allowlist check. The bare command after the prefix (git log, git diff, cat, etc.) was allowlisted, but the prefix at exec time mutated the downstream binary's behaviour to spawn the assigned value as a subprocess: GIT_PAGER=<cmd> git log, GIT_SSH_COMMAND=<cmd> git fetch, LESSOPEN=<cmd> cat, LD_PRELOAD=<lib> any-allowed-binary, BASH_ENV=<rc> any-shell-call, etc.

  4. Browser.grantPermissions(displayCapture) bypassed Chromium's gesture gate. The embedded Meet + Slack Huddles webviews had displayCapture in their auto-grant set. Pre-granting it via CDP defeats the transient-activation requirement Chromium normally enforces on navigator.mediaDevices.getDisplayMedia. A page inside the embedded origin could initiate a desktop capture programmatically — no click, no screen-picker. Auto-grant was originally added ([Feature] webview: Google Meet — full end-to-end parity with native app #1022 / feat(webview/gmeet): native cam/mic/screenshare + keep Meet routing in-app (#1022) #1054) to skip the cam/mic consent dialog; that justification holds for audioCapture/videoCapture but never applied to displayCapture, which has its own native screen-picker flow that should run on user gesture.

Solution

Three commits, scoped per-fix so each can be reverted independently:

  1. feat(security/tools): require can_act for node_exec and npm_exec — three-line guard at the top of each tool's execute(), mirroring the pattern from file_write.rs:56-59. Fires before the existing rate-limit check so the autonomy-block error message is not masked by an unrelated rate-limit error.

  2. feat(security/policy): block find -execdir/-okdir and dangerous env prefixes — extends the find blocklist with the two missing flags; adds DANGEROUS_ENV_PREFIXES (~30 names: pager / editor / SSH / diff hooks, BASH_ENV, ENV, PROMPT_COMMAND, PS1PS4, PYTHONSTARTUP, PYTHONPATH, LD_PRELOAD, LD_AUDIT, LD_LIBRARY_PATH, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FORCE_FLAT_NAMESPACE, PATH, SHELL, IFS); has_dangerous_env_prefix() rejects the segment if any of those names appear. Benign prefixes (TZ, LANG, LC_ALL, arbitrary user-named) still pass. Allowlist intentionally enumerates hooks known to spawn subprocesses or alter binary resolution — no attempt to enumerate every env var ever assigned. Case-insensitive matcher so git_pager=... and Ld_PrElOaD=... are also rejected.

  3. fix(cdp/session): drop displayCapture from origin auto-grants — removes "displayCapture" from the Meet + Slack pre-grant arrays in app/src-tauri/src/cdp/session.rs. audioCapture / videoCapture / clipboardReadWrite stay pre-granted (these are load-bearing for greenroom cam/mic auto-grant per [Feature] webview: Google Meet — full end-to-end parity with native app #1022 / feat(webview/gmeet): native cam/mic/screenshare + keep Meet routing in-app (#1022) #1054 and don't compromise the gesture gate the same way). User-visible flow on click is unchanged: clicking Meet's "Present" / Slack's huddle share button still fires getDisplayMedia, Chrome's native screen-picker shows on the click, the user picks a source, the stream lands. Only blocked path: the page calling getDisplayMedia programmatically without a gesture.

Submission Checklist

  • Tests added or updated — new dangerous_env_var_prefix_blocked covers every blocked env name plus the benign-prefix passthrough; existing command_argument_injection_blocked extended to cover -execdir and -okdir. 111 / 111 pass on cargo test --lib openhuman::security::policy::tests::.
  • Diff coverage ≥ 80% — every changed line in policy.rs and the policy module is covered by the extended + new tests; node_exec / npm_exec guards mirror the existing can_act() pattern that the surrounding tools already cover via their own ReadOnly tests; cdp/session.rs change is a deletion of three string literals from two arrays (no coverable code path added).
  • N/A: Coverage matrix updated — behaviour-only tightening of existing surfaces; no new feature row to add to docs/TEST-COVERAGE-MATRIX.md.
  • N/A: All affected feature IDs from the matrix are listed in ## Related — same reason as above.
  • No new external network dependencies introduced — three internal-only files touched.
  • N/A: Manual smoke checklist updated if this touches release-cut surfaces — no new release-cut surfaces. Adding a one-line note to docs/RELEASE-MANUAL-SMOKE.md to smoke Meet "Present" + Slack huddle screen-share after this change is queued as a follow-up.
  • N/A: Linked issue closed via Closes #NNN in the ## Related section — no public tracking issue (security-internal). Sentry / GHSA dashboard tracks the items off-PR.

Impact

  • Security: tightens three independent bypass classes in the action-tool / sandbox / embedded-webview perm surface. Lowers blast radius of prompt-injection-driven attacks against channel-bot ReadOnly deployments, allowlist-allowed binaries that hook env vars, and pages inside embedded Meet / Slack webviews.
  • Behaviour: node_exec / npm_exec now refuse cleanly in ReadOnly (same error string as the other tools); find -execdir / -okdir now refuse with the same code path as -exec / -ok; commands carrying a dangerous env prefix now refuse the same way an unallowlisted binary refuses; getDisplayMedia from Meet + Slack pages now requires a click (matches Chromium's default for non-pre-granted origins).
  • Performance: nil. Three constant-time checks added to existing per-segment validation; one shorter array on the CDP call.
  • Compatibility: dev tools, agent harness, and end-user UX paths are intentionally untouched. Tests cover both the new rejections and the benign passthroughs that must still work.

Related

  • Follows: feat: tighten runtime policy + transport guards #2331 (initial transport + memory + channels tightening, merged 2026-05-21)
  • Closes:
  • Follow-up PR(s)/TODOs:
    • Append a smoke line to docs/RELEASE-MANUAL-SMOKE.md for Meet "Present" + Slack huddle screen-share post-displayCapture-drop
    • Revisit DANGEROUS_ENV_PREFIXES when adding new allowlisted commands in src/openhuman/security/policy.rs

AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/runtime-policy-tightening-v2
  • Commit SHA: bf2b8bf0 (tip; 3 commits in the series)

Validation Run

  • pnpm --filter openhuman-app format:check — N/A: no TypeScript or app-workspace files touched (Rust core + Tauri shell only).
  • pnpm typecheck — N/A: same reason.
  • Focused tests: cargo test --lib openhuman::security::policy::tests::111 passed / 0 failed, including new dangerous_env_var_prefix_blocked and the extended command_argument_injection_blocked.
  • Rust fmt/check: cargo check --manifest-path Cargo.toml clean (pre-existing warnings on slack-backfill binary unchanged); cargo fmt --check clean on touched files.
  • Tauri fmt/check: cargo check --manifest-path app/src-tauri/Cargo.toml clean after git submodule update --init --recursive.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: three previously-reachable bypass paths now reject cleanly. No change to the legitimate dev / agent / end-user paths.
  • User-visible effect: ReadOnly-mode agents that previously could still spawn JS via node_exec / npm_exec now refuse with the standard "Action blocked: autonomy is read-only" error. getDisplayMedia inside Meet / Slack now requires a user gesture (matches Chromium default).

Parity Contract

  • Legacy behavior preserved: yes for every valid path. Cam/mic auto-grant on Meet + Slack unchanged. Benign env prefixes (TZ, LANG, LC_ALL, etc.) still pass the allowlist. find with legitimate args (-name, -type, -maxdepth, etc.) still passes.
  • Guard/fallback/dispatch parity checks: the new can_act() guards mirror the existing pattern from file_write / edit_file / curl / http_request exactly; reviewers can diff the new lines against any of those for visual parity.

Duplicate / Superseded PR Handling

Summary by CodeRabbit

  • Security Enhancements

    • Prevented command-injection via dangerous environment-variable prefix assignments
    • Blocked alternative find execution flags (including -execdir / -okdir)
  • Updates

    • Modified permission handling for Google Meet: clipboard access pre-granted; screen capture removed
    • Adjusted Slack Huddles permissions: screen capture removed
  • Tests

    • Added regression tests for env-var and find command-execution scenarios
    • Synchronized memory-tool tests to avoid parallel test interference

Review Change Stack

oxoxDev added 3 commits May 25, 2026 18:35
Mirrors the autonomy gate already enforced by file_write, edit_file,
apply_patch, curl, http_request, pushover, csv_export, and the rest of
the action-tool surface. Without this, an agent running under
AutonomyLevel::ReadOnly could still execute arbitrary JavaScript via
node_exec inline_code (and through it shell out to anything else),
silently defeating the read-only guarantee that channel-bot deployments
rely on.

Guard fires before the existing rate-limit check so an unrelated tool
denial cannot mask the autonomy-block error message.
…refixes

is_args_safe("find") previously rejected only -exec and -ok, leaving
-execdir and -okdir as a same-class bypass — they run an arbitrary
command per match with the working directory set to the match's
parent, identical execution semantics under a different option
spelling.

Separately, skip_env_assignments() let inline env-variable prefixes
(e.g. GIT_PAGER=<cmd> git log) pass the allowlist check. The bare
command is allowlisted but the prefix mutates how the downstream
binary executes — git, less, man, python and the dynamic loader all
have hook variables that turn an allowlisted binary into an arbitrary
command spawner. has_dangerous_env_prefix() encodes the set of names
that trigger this class (pager/editor/SSH/diff hooks, BASH_ENV,
PYTHONSTARTUP, LD_PRELOAD, LD_AUDIT, LD_LIBRARY_PATH, DYLD_*, PATH,
SHELL, IFS, PROMPT_COMMAND, and the PS1-PS4 family) and rejects the
whole segment when any of them are set.

Benign prefixes like TZ, LANG, LC_ALL, and arbitrary user-named
variables still pass — the allowlist deliberately enumerates only
the hook surfaces that are known to spawn subprocesses or alter
binary resolution.

Tests: dangerous_env_var_prefix_blocked covers every blocked name
plus the benign prefixes; command_argument_injection_blocked is
extended to cover -execdir and -okdir.
Browser.grantPermissions(displayCapture) bypasses Chromium's
transient-activation requirement on navigator.mediaDevices
.getDisplayMedia. With the pre-grant in place the embedded Meet and
Slack Huddles webviews can initiate a desktop capture programmatically
with no user gesture — the page never has to surface the
screen-picker, so a malicious script inside the embedded origin can
silently capture the desktop.

Removing displayCapture from the pre-grant set restores the gesture
gate. The user-visible flow stays the same: clicking Meet's
"Present" / Slack's huddle share button still fires
getDisplayMedia, Chrome's native screen-picker shows on the click,
the user picks a source, the stream lands. The only behavior change
is that the page can no longer call getDisplayMedia without a click.

audioCapture, videoCapture, and clipboardReadWrite stay pre-granted
for both origins — cam/mic auto-enumerate is the load-bearing piece
for Meet's greenroom and Slack's huddle pre-flight (tinyhumansai#1022 / tinyhumansai#1054)
and getUserMedia has its own permission model that the pre-grant
doesn't compromise the same way.
@oxoxDev oxoxDev requested a review from a team May 25, 2026 13:28
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cde45e0f-393a-4d75-b7a1-24d9d0c66abe

📥 Commits

Reviewing files that changed from the base of the PR and between efa9b5d and 12c8d48.

📒 Files selected for processing (1)
  • src/openhuman/memory_tools/tools/put.rs
✅ Files skipped from review due to trivial changes (1)
  • src/openhuman/memory_tools/tools/put.rs

📝 Walkthrough

Walkthrough

This PR blocks dangerous inline env-prefix command forms and additional find execution flags, updates per-origin CDP permission pre-grants to remove desktop-capture for Meet/Slack while keeping media/clipboard, and serializes two MemoryTools tests with a global test lock.

Changes

Command Execution Security Hardening

Layer / File(s) Summary
Dangerous environment-variable prefix detection
src/openhuman/security/policy.rs, src/openhuman/security/policy_tests.rs
Adds DANGEROUS_ENV_PREFIXES and has_dangerous_env_prefix to detect leading NAME=... env assignments and denies segments with dangerous names inside SecurityPolicy::is_command_allowed. Tests assert rejection for high-risk prefixes and allow benign forms.
Find command-execution pathway blocking
src/openhuman/security/policy.rs, src/openhuman/security/policy_tests.rs
Extends find argument validation to reject -execdir and -okdir in addition to -exec/-ok. Regression tests assert these variants are blocked.

Browser Permission Configuration

Layer / File(s) Summary
Embedded provider permission grants
app/src-tauri/src/cdp/session.rs
For meet.google.com and app.slack.com, remove displayCapture from Browser.grantPermissions pre-grants and add/adjust comments to document gesture-gate and clipboard/media intent; add clipboardReadWrite for Meet pre-grants.

Test synchronization

Layer / File(s) Summary
MemoryTools test lock acquisition
src/openhuman/memory_tools/tools/put.rs
Two async tests now acquire and await GLOBAL_MEMORY_TEST_LOCK before creating temp workspaces and running the tool to prevent concurrent test interference.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2013: Both PRs modify src/openhuman/security/policy.rs to harden command validation and block execution pathways.

Suggested reviewers

  • senamakel
  • sanil-23

Poem

🐰 I sniff the env and guard the gate,

No sneaky PREFIX gets to dictate.
Find's secret doors are barred and shut,
No desktop-capture in Meet or Huddles — what's up?
Tests now wait their turn, neat and small,
A rabbit hops in, keeping safe for all.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: tighten runtime policy + transport guards v2' directly and accurately summarizes the main security-focused changes across all modified files: enhanced sandbox policy, action-tool autonomy guards, and embedded-webview permission restrictions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.


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

@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. labels May 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/security/policy_tests.rs`:
- Around line 762-767: The failing CI is due to rustfmt formatting mismatches on
the multi-line assert! calls using p.is_command_allowed in
src/openhuman/security/policy_tests.rs; reformat these assertions to match
rustfmt (either collapse the assert! into a single line or let rustfmt reflow
the string literal) and then run cargo fmt --all (or rustfmt on that file)
before committing so the assert!(!p.is_command_allowed(...)) lines conform to
the project formatting rules.

In `@src/openhuman/tools/impl/system/node_exec.rs`:
- Around line 155-159: The conditional early-return formatting around
self.security.can_act() is failing rustfmt; adjust the formatting of the return
expression that builds ToolResult::error (the return Ok(ToolResult::error(...))
line inside the if block) to match rustfmt expectations or simply run `cargo fmt
--all` so rustfmt rewrites the block correctly; ensure the if block uses
standard Rust formatting (brace placement and indentation) and the call to
ToolResult::error is aligned per rustfmt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 452d9178-cbe5-4e48-bc45-48b2a921e235

📥 Commits

Reviewing files that changed from the base of the PR and between d997394 and bf2b8bf.

📒 Files selected for processing (5)
  • app/src-tauri/src/cdp/session.rs
  • src/openhuman/security/policy.rs
  • src/openhuman/security/policy_tests.rs
  • src/openhuman/tools/impl/system/node_exec.rs
  • src/openhuman/tools/impl/system/npm_exec.rs

Comment thread src/openhuman/security/policy_tests.rs
Comment thread src/openhuman/tools/impl/system/node_exec.rs
oxoxDev added 2 commits May 26, 2026 10:15
Collapse multi-line blocks rustfmt wanted single-line:
- src/openhuman/security/policy_tests.rs:761 (assert)
- src/openhuman/tools/impl/system/node_exec.rs:153 (ToolResult::error)
- src/openhuman/tools/impl/system/npm_exec.rs:181 (ToolResult::error)
- app/src-tauri/src/cdp/session.rs:539,560 (perms.extend_from_slice arrays)
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 26, 2026
…-tightening-v2

# Conflicts:
#	src/openhuman/tools/impl/system/node_exec.rs
#	src/openhuman/tools/impl/system/npm_exec.rs
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 26, 2026
Both async tests in `memory_tools::tools::put::tests` drive the
process-global memory client (`active_memory_client()`), but they did
not acquire `GLOBAL_MEMORY_TEST_LOCK` added in tinyhumansai#2649. Under cargo's
default parallel test execution they raced with the new
`memory::ops::*` tests that *do* hold the lock, picked up a polluted
cached client, and intermittently tripped the PII-rejection guard on
`tool-<name>` / `rule/<uuid>` writes — surfacing in CI as
`memory_tools_put: store tool rule: document namespace/key cannot
contain personal identifiers`.

Closes that flake door by acquiring the same serial lock at the top
of both tests, mirroring the pattern in `memory::ops::tool_memory::tests`.
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Review — graycyrus

Solid security hardening PR. Three independent bypass classes closed with clean, well-scoped fixes.

Walkthrough

Area Files What changed
Sandbox policy security/policy.rs New DANGEROUS_ENV_PREFIXES blocklist (~35 env vars) + has_dangerous_env_prefix() check runs before skip_env_assignments() in is_command_allowed(). Blocks pager/editor/SSH/loader/shell-eval env overrides that would let an allowlisted binary spawn attacker-controlled subprocesses. Also extends find arg blocklist with -execdir / -okdir.
Embedded webview perms cdp/session.rs displayCapture removed from Meet + Slack Browser.grantPermissions arrays. Chromium's transient-activation gate on getDisplayMedia now stays in force — screen-share still works via native picker on user click.
Tests policy_tests.rs Comprehensive coverage: every blocked env name, case-insensitive matching, benign prefix passthrough, -execdir/-okdir blocking.
Test stability memory_tools/tools/put.rs GLOBAL_MEMORY_TEST_LOCK added to serialize tests that share memory workspace state.

Assessment

  • has_dangerous_env_prefix() — correctly handles multiple chained env assignments (SAFE=x GIT_PAGER=bad git log), validates first char is alphabetic/underscore (so shell quoting tricks like 'GIT_PAGER'=x don't false-positive), and uses case-insensitive matching. The linear scan over ~35 entries is fine for a per-command check.
  • DANGEROUS_ENV_PREFIXES list — good coverage of git hooks, pager/editor, loader overrides (LD_PRELOAD, DYLD_*), shell-eval vars (BASH_ENV, PROMPT_COMMAND, IFS), Python startup, and PATH/SHELL. The GIT_EXTERNAL_FILTER entry doesn't correspond to a real git env var (filters use git config, not env), but it's harmless as an extra blocklist entry.
  • find blocklist-execdir/-okdir have identical code-execution semantics to -exec/-ok, blocking them is correct.
  • displayCapture removal — comments clearly explain the security rationale vs the preserved audioCapture/videoCapture/clipboardReadWrite grants.
  • CI: all relevant checks pass. The Windows secrets ACL failure is unrelated (no secrets/ACL code touched).
  • node_exec/npm_exec can_act() guards (commit 1) are already merged into main via upstream merge — confirmed the guard exists at ~line 155 of node_exec.rs on main.

No critical or major findings. Clean PR — moving to approval queue.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Looks good, nice work!

@graycyrus graycyrus merged commit 60050aa into tinyhumansai:main May 26, 2026
32 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants