Skip to content

feat(sandbox): unwired Docker-connect retry, egress allowlist, shell limits - #6746

Merged
henrypark133 merged 14 commits into
mainfrom
sandbox/slice1-connect-allowlist-limits
Jul 29, 2026
Merged

henrypark133 merged 14 commits into
mainfrom
sandbox/slice1-connect-allowlist-limits

Conversation

@henrypark133

Copy link
Copy Markdown
Collaborator

Why

The sandbox container transport has never been sliced to main. Six modules (~5,455 lines) live only on sandbox/shell-integration, and they block the sandbox-docker-tests CI job, the W6 credential swap, and three finished branches queued behind them.

This is slice 1 of 4 — the three leaf modules that depend on nothing else unlanded. Chosen and ordered by actual cargo build probes off origin/main, not by reading imports: this project has already been burned once by picking a slice whose files looked separable and whose symbols were not.

What's here

Module Lines Depends on
sandbox_process/connect.rs 311 bollard, new ironclaw_common dep
sandbox_process/network_allowlist.rs 155 ironclaw_host_api only
sandbox_process/shell_limits.rs 120 std only
  • connect — bounded-retry Docker connect (4 attempts, 250 ms doubling backoff), an IRONCLAW_REBORN_DOCKER_HOST override for CI/remote daemons, and a cheap readiness probe. Retry exhaustion propagates as a hard RuntimeProcessError; there is deliberately no unsandboxed-host fallback, and the module doc says so.
  • network_allowlist — the default sandboxed-shell egress allowlist (crates.io / npm / PyPI / Go proxy / GitHub) expressed as a NetworkPolicy, plus the IRONCLAW_SANDBOX_EXTRA_ALLOWED_DOMAINS operator hook.
  • shell_limits — builtin.shell's timeout and captured-output defaults, floors, and ceilings.

Unwired by design

All three land with no production caller, following the convention already merged in ca.rs and credential_firewall.rs: each module doc names its real future consumer, and no fake caller is invented to silence a lint.

  • connect → ironclaw_reborn_composition's sandbox_reaper_task and sandbox_composition's boot diagnostic.
  • network_allowlist → the egress proxy (slice 2) and sandbox_egress_proxy_task.
  • shell_limits' clamps → process_port, execute_in_container, and first_party_tools::shell.

The only behavior-adjacent change is sandbox_process's DEFAULT_TIMEOUT / DEFAULT_MAX_OUTPUT_BYTES now reading shell_limits' constants instead of repeating the same two literals — identical values.

Worth being explicit about one thing this PR does not do: shell_limits defines a 600 s timeout ceiling, but the live transport still applies a model-supplied timeout_secs with no ceiling at all. Wiring the clamp is a real behavior change and ships with its own test, in its own PR. It is not smuggled in behind this module's arrival.

Architecture ratchet

reborn_extension_specificity's PATH_TERM_COLLISIONS gains two entries for network_allowlist.rs. The allowlist names github.com and api.github.com as code hosts for ordinary git clone / release-archive workflows, not as the github extension — which is exactly the distinction that table exists to record. No ratchet count was weakened.

Test tier

Crate-tier only, which the testing rule endorses while these are unwired — there is no production path to drive. Integration-tier coverage is owed by the PR that gives each module a caller, and is noted per-module in the decomposition plan.

Gates

Baseline established on clean origin/main first, so pre-existing failures are distinguishable from new ones.

Gate Result
cargo build -p ironclaw_host_runtime clean
cargo test -p ironclaw_host_runtime --lib 502 passed, 5 failed — baseline on clean main was 487 passed, 5 failed. Same 5 by name: 2 known-failing first_party_tools::trace_commons onboard-guidance tests, 3 environmental SocketNotFoundError (no local Docker daemon). +15 new tests, 0 new failures.
cargo test -p ironclaw_architecture all suites pass
cargo fmt --all --check clean
cargo clippy -p ironclaw_host_runtime --all-targets -- -D warnings clean
cargo deny check advisories ok, bans ok, licenses ok, sources ok
python3 scripts/check_no_panics.py --base origin/main --head HEAD OK: No panic-inducing calls in changed production code.

ironclaw_common is promoted from transitive to direct here (connect reads its env-override helper). deny.toml sets unmaintained = "workspace", so that promotion was specifically checked against advisories — it is a workspace path dependency, and cargo deny is clean.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 28, 2026 04:23
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 28, 2026 04:23 Destroyed
@github-actions github-actions Bot added the scope: dependencies Dependency updates label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 58fe00ef-3f0c-411a-8204-20b3bdeae33c

📥 Commits

Reviewing files that changed from the base of the PR and between 0056083 and 44a0a58.

📒 Files selected for processing (1)
  • crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added configurable sandbox outbound networking controls: default domain allowlist, append-only extra domain patterns, and a per-request maximum egress byte cap.
    • Added hardened Docker-daemon connectivity with bounded retry and a readiness probe, including safer handling of the Docker endpoint override and remote access opt-in.
    • Added bounded sandbox shell execution limits for timeout and captured output size.
  • Documentation
    • Updated the environment example with new Docker and sandbox networking/egress settings.
  • Bug Fixes
    • Strengthened network-policy validation with clearer errors for invalid host-pattern and egress-limit inputs.
  • Tests
    • Expanded contract and validation coverage for host-pattern parsing, plus added connectivity/readiness regression tests.

Walkthrough

The sandbox runtime adds bounded Docker connectivity and readiness checks, configurable egress policies, centralized shell-limit clamping, stricter network-policy parsing, and public re-exports for the new helpers.

Changes

Sandbox runtime hardening

Layer / File(s) Summary
Network policy parsing and validation
crates/ironclaw_network/src/policy.rs, crates/ironclaw_network/src/lib.rs, crates/ironclaw_network/tests/network_policy_contract.rs, crates/ironclaw_host_api/src/action.rs
Network policy errors and parsing helpers validate host patterns and positive egress limits, with contract tests and documentation distinguishing configuration parsing from declaration validation.
Sandbox network policy
crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs, crates/ironclaw_architecture/tests/reborn_extension_specificity.rs
Default and operator-provided domains are merged into a private-IP-blocking policy with a configurable egress ceiling; GitHub terms are exempted from specificity checks in the allowlist module.
Docker connectivity and readiness
crates/ironclaw_host_runtime/Cargo.toml, crates/ironclaw_host_runtime/src/sandbox_process/connect.rs, .env.example
Docker overrides honor endpoint trust boundaries, supported sockets are probed, retries use bounded exponential backoff, readiness is reported without leaking endpoints, and exhaustion returns an error.
Shell limits and public runtime surface
crates/ironclaw_host_runtime/src/sandbox_process/shell_limits.rs, crates/ironclaw_host_runtime/src/sandbox_process.rs, crates/ironclaw_host_runtime/src/lib.rs
Shell timeout and output values are defaulted and clamped, while connectivity and network-policy APIs are re-exported.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SandboxRuntime
  participant connect_docker_with_retry
  participant connect_once
  participant DockerDaemon
  SandboxRuntime->>connect_docker_with_retry: request sandbox Docker connection
  connect_docker_with_retry->>connect_once: run bounded retry attempt
  connect_once->>DockerDaemon: connect and ping
  DockerDaemon-->>connect_once: success or connection failure
  connect_docker_with_retry-->>SandboxRuntime: Docker connection or hard error
Loading

Possibly related issues

  • nearai/ironclaw issue 6473: Covers Docker socket reachability and retry handling implemented in sandbox_process::connect.

Possibly related PRs

  • nearai/ironclaw#6584: Touches the same sandbox network allowlist and related sandbox_process plumbing.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it misses several required template sections like Change Type, Linked Issue, Validation checklist, and the trust-boundary checklist. Add the missing template sections, especially Change Type, Linked Issue, Validation, Test Strategy, Security Impact, Trust-Boundary Checklist, and Rollback Plan.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The Conventional Commits-style title accurately summarizes the sandbox transport slice changes.
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.

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

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

@github-actions github-actions Bot added size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 55-58: Update SandboxDockerReadiness and its construction sites to
avoid exposing raw Docker errors in the public Unreachable state. Remove the
reason payload, log the detailed error internally at debug! with appropriate
redaction, and preserve the opaque Ready/Unreachable readiness contract.

In `@crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs`:
- Around line 91-103: Update sandbox_network_policy to replace the unbounded
max_egress_bytes: None with the defined sandbox egress ceiling, using the
existing configuration or constant if available. Add a regression test for
sandbox_network_policy that verifies the generated NetworkPolicy carries this
bounded value.
- Around line 62-72: Update sandbox_extra_allowed_domains to distinguish an
unset SANDBOX_EXTRA_ALLOWED_DOMAINS_ENV variable from an invalid non-Unicode
value: return the empty allowlist only for NotPresent, and fail with a
contextual configuration error for NotUnicode instead of silently defaulting.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d8945b9c-9cb7-40a2-8ee2-bfb6f6ff129a

📥 Commits

Reviewing files that changed from the base of the PR and between bc0726d and e7fcd28.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (7)
  • crates/ironclaw_architecture/tests/reborn_extension_specificity.rs
  • crates/ironclaw_host_runtime/Cargo.toml
  • crates/ironclaw_host_runtime/src/lib.rs
  • crates/ironclaw_host_runtime/src/sandbox_process.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/shell_limits.rs

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/connect.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs Outdated
@ironloopai

ironloopai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #6746

⚫ Cancelled · Target changed

The target changed before this Run could finish.

Automatic · PR opened + CI failed · attempt 1 of 3 · cancelled after 47s

Run details
  • Repository: nearai/ironclaw
  • Base: main at bc0726d
  • Head: sandbox/slice1-connect-allowlist-limits at e7fcd28
  • Created: Jul 28, 2026, 4:28 AM UTC
  • Updated: Jul 28, 2026, 4:29 AM UTC
  • Run: 469e1747-12de-4b7e-b1e1-7d308f05baa0
  • Latest attempt: 1 · Cancelled · 97275c75-604c-451c-8c5e-9a2a464627ae

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7fcd28fff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +66 to +70
raw.split(',')
.map(str::trim)
.filter(|domain| !domain.is_empty())
.map(str::to_string)
.collect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject wildcard domains before constructing the allowlist

When IRONCLAW_SANDBOX_EXTRA_ALLOWED_DOMAINS=*, this parser forwards * unchanged; NetworkTargetPattern explicitly accepts that value and ironclaw_network::host_matches_pattern treats it as matching every host. Once this policy is wired to the documented proxy consumer, a single configuration typo or overly broad value therefore turns the public-internet allowlist into allow-all, while deny_private_ip_ranges blocks only private addresses. Restrict extras to hostname and *. suffix forms and fail closed on invalid entries.

AGENTS.md reference: AGENTS.md:L128-L135

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already fixed on this branch: 9240ae8 hard-fails sandbox_extra_allowed_domains when the operator env var contains a bare * (via parse_host_pattern's explicit wildcard rejection, tested by extra_domains_env_rejects_bare_wildcard). Not resolving — leaving for reviewer confirmation.

Comment on lines +62 to +64
pub fn sandbox_extra_allowed_domains() -> Vec<String> {
std::env::var(SANDBOX_EXTRA_ALLOWED_DOMAINS_ENV)
.ok()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor the shared environment overlay for extra domains

In deployments using the workspace runtime environment overlay, this direct std::env::var lookup bypasses runtime masks, runtime-only values, and the registered fallback. In particular, masking an inherited IRONCLAW_SANDBOX_EXTRA_ALLOWED_DOMAINS value still leaves those domains authorized, while a runtime-provided value is silently ignored; use ironclaw_common::env_helpers::env_or_override as the new Docker-host path does so configuration precedence remains fail-closed.

AGENTS.md reference: AGENTS.md:L121-L124

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already fixed on this branch: dc2fc40 switched sandbox_extra_allowed_domains from raw std::env::var to env_or_override, the same runtime-override-honoring lookup connect::DOCKER_HOST_ENV uses — it now respects masks/runtime-only values/fallback. Not resolving — leaving for reviewer confirmation.

Comment on lines +198 to +200
Err(err) => SandboxDockerReadiness::Unreachable {
reason: err.to_string(),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sanitize Docker readiness failures

When an explicit Docker override is unreachable, connect_override embeds the configured socket path or remote daemon address plus Bollard's backend error in RuntimeProcessError, and this readiness result publishes the entire string for the planned startup log or health consumer. That leaks raw host paths/private endpoints and backend details across a public diagnostic surface; retain a stable sanitized reason code here and keep the detailed cause restricted to an appropriately protected internal diagnostic.

AGENTS.md reference: crates/ironclaw_host_runtime/AGENTS.md:L25-L28

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already fixed on this branch: 0aa87eb sanitizes every connect_override error at the source — all five constructors now emit fixed "{env var} <unix socket|http endpoint> <connect|ping> failed" text, never err.to_string() or the raw host/endpoint. SandboxDockerReadiness.reason is downstream of that and carries only the sanitized text. Verified: no .to_string() on a Bollard/backend error remains in connect_override or its callers. Not resolving — leaving for reviewer confirmation.

Comment on lines +113 to +114
let docker =
Docker::connect_with_http(host, 120, bollard::API_DEFAULT_VERSION).map_err(|e| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound each Docker connection attempt

When the configured remote daemon accepts a TCP connection but never answers, or packets are black-holed, each ping() inherits Bollard's 120-second client timeout. Four sequential retry attempts can consequently block for roughly eight minutes before the short backoffs are considered, and the supposedly fast one-shot readiness probe can block for two minutes. Wrap each complete attempt in a much shorter timeout, or lower the Bollard client timeout, so the advertised few-second bound actually covers connection work as well as retry sleeps.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already fixed on this branch: 021467f moved CONNECT_ATTEMPT_TIMEOUT (5s) into connect_once itself, so connect_docker_with_retry's retry loop (run_with_retry(MAX_ATTEMPTS=4, ...)) now bounds each attempt (dial + ping, including the local-default branch and the whole unix-socket-candidate loop) to 5s instead of inheriting Bollard's 120s client timeout — 4 attempts is now ~20s + backoff, not ~8 minutes. Same commit/test as the sibling readiness-probe thread (sandbox_docker_readiness_bounded_when_daemon_accepts_but_never_answers). Not resolving — leaving for reviewer confirmation.

Copilot AI review requested due to automatic review settings July 28, 2026 04:29
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 28, 2026 04:29 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_host_runtime/src/sandbox_process/connect.rs (1)

102-133: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep plaintext Docker access loopback-only in code and documentation.

The unauthenticated transport must not be usable or advertised for remote Docker daemons.

  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs#L102-L133: reject non-loopback HTTP/TCP overrides before dialing, or add TLS/mTLS.
  • .env.example#L253-L258: remove remote-daemon guidance and restrict examples to Unix sockets or loopback.

As per path instructions, network authority must fail closed and documented guarantees must match enforcement.

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs` around lines 102
- 133, Update connect_override to reject non-loopback HTTP/TCP hosts before
Docker::connect_with_http dials them; allow only loopback addresses or Unix
sockets, failing closed for remote plaintext endpoints. In .env.example lines
253-258, remove remote-daemon guidance and document only Unix-socket or loopback
overrides so the documented configuration matches enforcement.

Source: Path instructions

♻️ Duplicate comments (2)
crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs (1)

91-109: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bound sandbox egress bytes.

max_egress_bytes: None leaves downloads unbounded for every allowed host. Set a finite ceiling and assert it in sandbox_network_policy_is_non_empty_and_denies_private_ips.

As per path instructions, network payloads require explicit resource bounds.

Also applies to: 135-149

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs` around
lines 91 - 109, Update sandbox_network_policy to set max_egress_bytes to a
finite resource limit instead of None, then extend
sandbox_network_policy_is_non_empty_and_denies_private_ips to assert that
configured ceiling. Apply the same bounded-egress expectation to the related
policy construction at the additional referenced location.

Source: Path instructions

crates/ironclaw_host_runtime/src/sandbox_process/connect.rs (1)

28-67: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep Docker readiness opaque.

Unreachable { reason: String } publishes err.to_string() across the host-runtime surface, and the readiness test locks in that raw diagnostic contract. Remove the reason payload or replace it with a stable sanitized code; keep detailed errors in internal debug! logs.

As per path instructions, public surfaces must not expose raw transport internals or unredacted runtime content.

Also applies to: 198-210, 328-355

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs` around lines 28
- 67, Make SandboxDockerReadiness opaque by removing the raw String payload from
Unreachable or replacing it with a stable sanitized error code, and update all
constructors, matches, and readiness tests accordingly. In the Docker
readiness/connect flow, retain detailed transport errors only in internal debug!
logs and ensure no err.to_string() or unredacted runtime content crosses the
host-runtime surface.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 102-133: Update connect_override to reject non-loopback HTTP/TCP
hosts before Docker::connect_with_http dials them; allow only loopback addresses
or Unix sockets, failing closed for remote plaintext endpoints. In .env.example
lines 253-258, remove remote-daemon guidance and document only Unix-socket or
loopback overrides so the documented configuration matches enforcement.

---

Duplicate comments:
In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 28-67: Make SandboxDockerReadiness opaque by removing the raw
String payload from Unreachable or replacing it with a stable sanitized error
code, and update all constructors, matches, and readiness tests accordingly. In
the Docker readiness/connect flow, retain detailed transport errors only in
internal debug! logs and ensure no err.to_string() or unredacted runtime content
crosses the host-runtime surface.

In `@crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs`:
- Around line 91-109: Update sandbox_network_policy to set max_egress_bytes to a
finite resource limit instead of None, then extend
sandbox_network_policy_is_non_empty_and_denies_private_ips to assert that
configured ceiling. Apply the same bounded-egress expectation to the related
policy construction at the additional referenced location.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d3f566a0-feb0-4353-b039-0ed9bb4135a9

📥 Commits

Reviewing files that changed from the base of the PR and between e7fcd28 and f176f4a.

📒 Files selected for processing (3)
  • .env.example
  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs

@railway-app

railway-app Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-6746 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 29, 2026 at 12:14 am

@github-actions

github-actions Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.77% (315232 / 367545 lines)
  floor:    80.81% (tolerance 0.5pp -> effective floor 80.31%)
  denominator: 367545 lines now vs 377084 at floor capture (-9539 lines, -2.53%) — not a material change

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_prompt_envelope, ironclaw_scripts

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.77% — 315232 / 367545 lines

Per-crate breakdown (60 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_process_sandbox 33.91% 118 / 348
ironclaw_host_ingress 42.5% 17 / 40
ironclaw_event_projections 43.51% 684 / 1572
ironclaw_observability 61.54% 16 / 26
ironclaw_authorization 63.02% 610 / 968
ironclaw_memory 64.41% 959 / 1489
ironclaw_telegram_v2_adapter 69.69% 731 / 1049
ironclaw_trust 73.21% 664 / 907
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_filesystem 74.64% 4829 / 6470
ironclaw_extractors 74.72% 538 / 720
ironclaw_capabilities 75.3% 2878 / 3822
ironclaw_projects 76.48% 400 / 523
ironclaw_mcp 76.6% 779 / 1017
ironclaw_reborn_cli 78.36% 10782 / 13760
ironclaw_wasm 79.72% 735 / 922
ironclaw_llm 80.14% 22266 / 27783
ironclaw_memory_native 81.02% 3299 / 4072
ironclaw_auth 81.88% 6679 / 8157
ironclaw_first_party_extensions 82.38% 6682 / 8111
ironclaw_processes 83.3% 933 / 1120
ironclaw_host_api 83.7% 9722 / 11615
ironclaw_reborn_identity 83.8% 450 / 537
ironclaw_events 84.06% 1687 / 2007
ironclaw_operator 84.41% 5561 / 6588
ironclaw_secrets 84.56% 2798 / 3309
ironclaw_network 85.07% 957 / 1125
ironclaw_reborn_config 85.23% 2101 / 2465
ironclaw_skills 85.27% 4493 / 5269
ironclaw_extension_host 85.34% 19110 / 22393
ironclaw_telegram_extension 85.4% 1299 / 1521
ironclaw_run_state 85.77% 458 / 534
ironclaw_reborn_composition 85.88% 24758 / 28830
ironclaw_triggers 85.92% 2783 / 3239
ironclaw_reborn_event_store 86.17% 1246 / 1446
ironclaw_webui 86.37% 11171 / 12934
ironclaw_hooks 86.72% 9949 / 11472
ironclaw_common 86.99% 1772 / 2037
ironclaw_approvals 87.07% 1542 / 1771
ironclaw_extensions 87.24% 4798 / 5500
ironclaw_threads 87.36% 4912 / 5623
ironclaw_product 87.48% 20324 / 23233
ironclaw_reborn_traces 88.13% 11987 / 13601
ironclaw_turns 88.49% 14560 / 16453
ironclaw_host_runtime 88.98% 20449 / 22982
ironclaw_slack_extension 89.26% 2027 / 2271
ironclaw_reborn_openai_compat 89.32% 3780 / 4232
ironclaw_conversations 90.03% 3171 / 3522
ironclaw_resources 90.84% 4474 / 4925
ironclaw_event_streams 91.24% 1063 / 1165
ironclaw_runner 91.32% 17366 / 19017
ironclaw_loop_host 91.98% 16604 / 18051
ironclaw_attachments 93.06% 630 / 677
ironclaw_outbound 93.91% 4101 / 4367
ironclaw_agent_loop 94.51% 10056 / 10640
ironclaw_safety 95.22% 3941 / 4139
ironclaw_first_party_extension_ports 95.62% 3672 / 3840
ironclaw_runtime_policy 96.56% 814 / 843

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (3 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

Copilot AI review requested due to automatic review settings July 28, 2026 06:19
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 28, 2026 06:19 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 241-258: Update sandbox_docker_readiness() to execute
connect_once() within the module’s CONNECT_ATTEMPT_TIMEOUT using
tokio::time::timeout. Preserve Ready for successful connections, map timeout and
connection errors to Unreachable with the existing fixed reason, and retain
debug logging for the resulting failure.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 25ac48e8-cbc0-43fa-b6d7-0151f8644632

📥 Commits

Reviewing files that changed from the base of the PR and between f176f4a and 5057f3c.

📒 Files selected for processing (1)
  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/connect.rs
Copilot AI review requested due to automatic review settings July 28, 2026 06:41
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 28, 2026 06:41 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 28, 2026 06:42
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 28, 2026 06:42 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_host_runtime/src/sandbox_process/connect.rs (1)

150-216: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use a canonical URL parser for the Docker host permit decision.

docker_host_component/is_loopback_docker_host hand-roll host extraction before deciding whether plaintext Docker API access is host-root-equivalent. This violates the crates/ironclaw_host_runtime invariant: “Compose low-level services such as ironclaw_network and ironclaw_secrets; do not duplicate URL parsing, DNS checks, private-IP filtering, HTTP clients, secret stores, or redaction logic in runtime crates.”

The bracket parsing also has an acceptability gap: [127.0.0.1]evil.com:2375 normalizes the loopback check to 127.0.0.1 while the unmodified host string passed to Docker::connect_with_http still contains evil.com. Extract the host with url::Url/ironclaw_network and classify the host itself; malformed Docker endpoint values should be rejected, not accepted through a split host check.

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs` around lines 150
- 216, Replace the hand-rolled docker_host_component/is_loopback_docker_host
parsing with canonical URL parsing via the existing url/ironclaw_network
utilities, and classify the exact parsed host used by Docker::connect_with_http.
Reject malformed or ambiguous Docker endpoint values before the remote-host
permit check, ensuring bracketed or otherwise deceptive inputs cannot be
normalized into an unrelated loopback host.

Source: Path instructions

♻️ Duplicate comments (1)
crates/ironclaw_host_runtime/src/sandbox_process/connect.rs (1)

305-322: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

sandbox_docker_readiness() still awaits connect_once() without an outer timeout.

Unlike connect_docker_with_retry (lines 288-298), which wraps each attempt in tokio::time::timeout(CONNECT_ATTEMPT_TIMEOUT, ...), this readiness probe calls connect_once().await directly. connect_once's local-discovery branch uses Docker::connect_with_local_defaults(), which has no explicit timeout argument and defaults to Bollard's internal 2-minute request timeout. A Docker daemon that accepts a connection but never answers ping() can block this boot-diagnostic probe for up to ~2 minutes — the same concern raised in a prior review round on this file that appears not yet addressed here.

⏱️ Suggested fix
 pub async fn sandbox_docker_readiness() -> SandboxDockerReadiness {
-    match connect_once().await {
+    match tokio::time::timeout(CONNECT_ATTEMPT_TIMEOUT, connect_once())
+        .await
+        .unwrap_or_else(|_elapsed| {
+            Err(RuntimeProcessError::ExecutionFailed(format!(
+                "docker readiness probe timed out after {CONNECT_ATTEMPT_TIMEOUT:?}"
+            )))
+        })
+    {
         Ok(_) => SandboxDockerReadiness::Ready,
         Err(err) => {
🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs` around lines 305
- 322, Wrap the connect_once() await in sandbox_docker_readiness with
tokio::time::timeout using the existing CONNECT_ATTEMPT_TIMEOUT constant, and
handle timeout as an unreachable readiness result while preserving the current
error logging and fixed public reason for ordinary failures.
🤖 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 `@crates/ironclaw_host_runtime/src/lib.rs`:
- Around line 135-141: The public runtime API re-exports
connect_docker_with_retry, exposing a raw bollard::Docker client that bypasses
sandbox mediation. Remove it from the public re-exports and keep the helper
private or crate-visible; expose only sandbox readiness data or a
RebornScopedSandboxCommandTransport-style factory through the runtime API.

---

Outside diff comments:
In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 150-216: Replace the hand-rolled
docker_host_component/is_loopback_docker_host parsing with canonical URL parsing
via the existing url/ironclaw_network utilities, and classify the exact parsed
host used by Docker::connect_with_http. Reject malformed or ambiguous Docker
endpoint values before the remote-host permit check, ensuring bracketed or
otherwise deceptive inputs cannot be normalized into an unrelated loopback host.

---

Duplicate comments:
In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 305-322: Wrap the connect_once() await in sandbox_docker_readiness
with tokio::time::timeout using the existing CONNECT_ATTEMPT_TIMEOUT constant,
and handle timeout as an unreachable readiness result while preserving the
current error logging and fixed public reason for ordinary failures.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2150d5cc-d4bb-4a9a-9631-fdae73f9ce9e

📥 Commits

Reviewing files that changed from the base of the PR and between 5057f3c and 73ea565.

📒 Files selected for processing (8)
  • .env.example
  • crates/ironclaw_host_runtime/src/lib.rs
  • crates/ironclaw_host_runtime/src/sandbox_process.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs
  • crates/ironclaw_network/src/lib.rs
  • crates/ironclaw_network/src/policy.rs
  • crates/ironclaw_network/tests/network_policy_contract.rs

Comment on lines +135 to +141
DEFAULT_SANDBOX_ALLOWED_DOMAINS, DEFAULT_SANDBOX_MAX_EGRESS_BYTES, RebornSandboxConfig,
RebornSandboxContainerIdentity, RebornSandboxNetworkBroker, RebornSandboxScopeKey,
RebornSandboxSecretBroker, RebornSandboxUserKey, RebornSandboxWorkspaceMode,
RebornScopedSandboxCommandTransport, SANDBOX_EXTRA_ALLOWED_DOMAINS_ENV,
SANDBOX_MAX_EGRESS_BYTES_ENV, SandboxActivityRegistry, SandboxDockerReadiness,
connect_docker_with_retry, sandbox_allowed_domains, sandbox_docker_readiness,
sandbox_extra_allowed_domains, sandbox_max_egress_bytes, sandbox_network_policy,

@coderabbitai coderabbitai Bot Jul 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
sed -n '120,150p' crates/ironclaw_host_runtime/src/lib.rs || true

echo "== locate connect_docker_with_retry definition & visibility =="
rg -n "connect_docker_with_retry|pub use .*Docker|use.*Docker|struct Docker|RebornScopedSandboxCommandTransport" crates/ironclaw_host_runtime crates -g '*.rs' | head -200

echo "== relevant module context =="
sed -n '1,220p' crates/ironclaw_host_runtime/src/lib.rs | nl -ba | sed -n '1,240p'

echo "== find public exports of ironclaw_host_runtime / usage of connect_docker_with_retry =="
rg -n "ironclaw_host_runtime::|crate::.*host_runtime|connect_docker_with_retry|Docker" --glob '*.rs' .

Repository: nearai/ironclaw

Length of output: 9823


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect likely definitions =="
fd -a '.*\.rs$' crates/ironclaw_host_runtime/src | while read -r f; do
  if rg -q 'connect_docker_with_retry|RebornScopedSandboxCommandTransport|SandboxDockerReadiness|RebornSandboxConfig|RebornSandboxWorkspaceMode' "$f"; then
    echo "--- $f"
    wc -l "$f"
    rg -n "connect_docker_with_retry|RebornScopedSandboxCommandTransport|SandboxDockerReadiness|RebornSandboxConfig|RebornSandboxWorkspaceMode|pub (async fn|fn|struct|enum|type)|Docker" "$f"
  fi
done

Repository: nearai/ironclaw

Length of output: 12250


Do not expose a raw Docker client through the public runtime API.

connect_docker_with_retry returns a bollard::Docker client and is re-exported at crates/ironclaw_host_runtime/src/lib.rs:140; this violates the sandbox/process guidance that container control must not bypass typed policy or host mediation. Keep it private/crate-visible and expose readiness data or a RebornScopedSandboxCommandTransport-style factory instead.

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/lib.rs` around lines 135 - 141, The public
runtime API re-exports connect_docker_with_retry, exposing a raw bollard::Docker
client that bypasses sandbox mediation. Remove it from the public re-exports and
keep the helper private or crate-visible; expose only sandbox readiness data or
a RebornScopedSandboxCommandTransport-style factory through the runtime API.

Source: Path instructions

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid concern, not fixing here — this is a design fork, not a bug in what's shipped. connect_docker_with_retry returns bollard::Docker and is re-exported at the crate root, but the module doc already flags it as "ships unwired": its only documented future consumers (sandbox_reaper_task, sandbox_composition's boot diagnostic) live in ironclaw_reborn_composition, which doesn't exist yet, and the doc explicitly defers repointing the live transport at this path to "the PR that carries a test for it." Whether the reaper genuinely needs raw Docker control-plane access (container list/remove for cleanup) versus a narrower RebornScopedSandboxCommandTransport-style port is a call best made when that consumer is actually built and its needs are concrete, not preemptively on an unwired export with no caller today. Tightening visibility now (private/crate-only) would just break the composition crate's ability to call it once wired, forcing the same decision back open anyway. Flagging for the wiring PR to resolve explicitly rather than deciding it unilaterally here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@henrypark133, understood that the actual consumer contract is deferred. The finding remains valid here, though: a crate-root pub use is a shipped public API commitment even when it has no caller yet.

This conflicts with the sandbox invariant in the repository guidance: runtime/container control must cross typed host APIs and host mediation, rather than exposing a raw Docker client. A future composition crate needing cross-crate access is precisely when the narrowly scoped control-plane port/factory should be designed—not a reason to publish bollard::Docker now.

I’ll leave this unresolved for the wiring PR. Would you like me to open a follow-up issue with acceptance criteria to replace the raw export with a typed, scoped Docker control-plane boundary before it gains a consumer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not creating a follow-up issue from here — no new evidence changes the call since my last reply: the export has no caller anywhere in the tree today (grep confirms connect_docker_with_retry is referenced only within ironclaw_host_runtime itself), and the concrete shape of a scoped Docker control-plane port depends on what sandbox_reaper_task/sandbox_composition's boot diagnostic actually need once ironclaw_reborn_composition exists to call it. I'll leave this thread open and unresolved for the wiring PR to pick up explicitly, as you suggested, rather than either implementing a narrowing now or spinning up separate issue tracking for an unwired export.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@henrypark133, acknowledged. With no current external caller or wired transport path, this is not an active bypass in the shipped execution flow.

The boundary concern remains deferred: crates/ironclaw_host_runtime/src/lib.rs publishes a bollard::Docker-returning API, while the sandbox invariant requires container control to cross typed host APIs, authorization, and the owning runtime adapter. The wiring PR should choose the least-authority control-plane contract required by the actual reaper/readiness consumer and add a caller-level test through that path.

Leaving this thread unresolved for that PR, with no separate issue.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

@ironloopai

ironloopai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #6746

🟢 Completed · Review submitted

1 actionable findings →

Reviewed the complete trusted base-to-head comparison. The allowlist validation, egress-limit parsing, shell constants, dependency/export changes, and architecture ratchet are coherent. One security-sensitive diagnostic leak remains in Docker connection errors.

Manual command by @henrypark133 · attempt 1 of 3 · completed in 2m 46s

Run details
  • Repository: nearai/ironclaw
  • Base: main at 7459ed3
  • Head: sandbox/slice1-connect-allowlist-limits at 6987a94
  • Created: Jul 28, 2026, 5:54 PM UTC
  • Updated: Jul 28, 2026, 5:57 PM UTC
  • Run: 5231e5b8-5e5b-44d5-88de-72f59101e1c6
  • Latest attempt: 1 · Completed · 29f6ce04-3cb7-4d73-94a0-1ca3b9e43603

@ironloopai ironloopai 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.

🔍 Review complete · PR #6746

⚠️ 1 finding · 1 blocking

Reviewed the complete trusted base-to-head comparison. The allowlist validation, egress-limit parsing, shell constants, dependency/export changes, and architecture ratchet are coherent. One security-sensitive diagnostic leak remains in Docker connection errors.

Findings

  1. 🟠 Medium · Docker connection errors expose configured endpoints and raw backend details — crates/ironclaw_host_runtime/src/sandbox_process/connect.rs:159-166
    Details are attached to the relevant diff.
Validation and technical details
  • Compared refs/ironloop/base (7459ed3) through refs/ironloop/head (6987a94), covering all 13 changed files.
  • Read repository and crate-local guidance for ironclaw_host_runtime, ironclaw_network, ironclaw_host_api, and ironclaw_architecture.
  • Traced all new exported functions and confirmed the Docker retry/readiness and sandbox policy helpers remain unwired outside their modules in this comparison.
  • Inspected policy matching, private-address enforcement, environment override precedence, timeout behavior, retry arithmetic, and added unit/contract tests.
  • Verified the checked-out merge tree matches the trusted head version of the principal changed module and git diff --check reports no whitespace errors.
  • Targeted Cargo tests could not be executed because cargo is unavailable in the review environment (/bin/bash: cargo: command not found).
  • Base: main
  • Head: sandbox/slice1-connect-allowlist-limits at 6987a94
  • Run: 5231e5b8-5e5b-44d5-88de-72f59101e1c6

Comment on lines +159 to +166
RuntimeProcessError::ExecutionFailed(format!(
"{DOCKER_HOST_ENV} unix socket connect failed for {host}: {e}"
))
})?;
docker.ping().await.map_err(|e| {
RuntimeProcessError::ExecutionFailed(format!(
"{DOCKER_HOST_ENV} unix socket ping failed for {host}: {e}"
))

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.

🟠 Medium · Docker connection errors expose configured endpoints and raw backend details

connect_override embeds the complete IRONCLAW_REBORN_DOCKER_HOST value and Bollard's raw error in RuntimeProcessError. The readiness path subsequently writes that error to debug logs, and retry consumers receive it unchanged. This exposes raw host socket paths/network topology and can disclose credentials if an operator supplies a URL containing user-info. It also conflicts with this crate's rule prohibiting raw host paths and backend errors in errors or logs. Sanitize these errors at their source: retain only a fixed endpoint kind and safe failure category, while keeping sensitive details out of runtime-visible errors and logging fields.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already fixed on this branch: 0aa87eb. connect_override no longer embeds IRONCLAW_REBORN_DOCKER_HOST's value or Bollard's raw error anywhere — every branch (unix-socket connect/ping, http connect/ping, non-loopback rejection) returns fixed text naming only the env var, endpoint kind, and failure category. The non-loopback rejection (the branch that used to echo user:pass@host verbatim) is covered by connect_override_http_errors_never_leak_host_or_userinfo_credential. Not resolving — leaving for reviewer confirmation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs (1)

62-67: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale claim: PolicyNetworkHttpEgress is not the sole consumer of this constant.

DEFAULT_SANDBOX_MAX_EGRESS_BYTES flows through sandbox_max_egress_bytes() (Line 164) into sandbox_network_policy() (Line 192). PolicyNetworkHttpEgress is the semantic reference for estimated_bytes, not a consumer of this constant. This was flagged previously and marked addressed, but the wording is still here.

📝 Suggested wording
-/// That per-request model holds today only because the sole consumer of
-/// this constant is `PolicyNetworkHttpEgress`, a host-mediated HTTP client
+/// This constant currently feeds `sandbox_network_policy`; its per-request
+/// semantics are borrowed from `PolicyNetworkHttpEgress`, a host-mediated HTTP client

As per coding guidelines: "comments/documentation promising guarantees must match code and tests".

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs` around
lines 62 - 67, Update the documentation above DEFAULT_SANDBOX_MAX_EGRESS_BYTES
to remove the claim that PolicyNetworkHttpEgress is its sole consumer. Describe
PolicyNetworkHttpEgress only as the semantic reference for estimated_bytes, and
accurately acknowledge the constant’s flow through sandbox_max_egress_bytes()
and sandbox_network_policy().

Source: Coding guidelines

🤖 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 `@crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs`:
- Around line 131-153: Update sandbox_extra_allowed_domains and
sandbox_allowed_domains to return validated NetworkTargetPattern values instead
of String values. Preserve the NetworkTargetPattern instances produced by
parse_host_pattern without extracting host_pattern, and route
DEFAULT_SANDBOX_ALLOWED_DOMAINS through the same parser. Remove the manual
field-by-field reconstruction so scheme and port remain governed by the
validator.

In `@crates/ironclaw_network/src/policy.rs`:
- Around line 116-149: Update parse_host_pattern to reject trimmed host patterns
exceeding the existing 253-byte DNS limit before constructing
NetworkTargetPattern. Preserve the current empty, wildcard, and shape validation
behavior, and use invalid_host_pattern with an appropriate length-bound error.

---

Duplicate comments:
In `@crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs`:
- Around line 62-67: Update the documentation above
DEFAULT_SANDBOX_MAX_EGRESS_BYTES to remove the claim that
PolicyNetworkHttpEgress is its sole consumer. Describe PolicyNetworkHttpEgress
only as the semantic reference for estimated_bytes, and accurately acknowledge
the constant’s flow through sandbox_max_egress_bytes() and
sandbox_network_policy().
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b84aaae2-6a9f-4cb3-b259-bba0262bfc5b

📥 Commits

Reviewing files that changed from the base of the PR and between 021467f and 6987a94.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (12)
  • .env.example
  • crates/ironclaw_architecture/tests/reborn_extension_specificity.rs
  • crates/ironclaw_host_api/src/action.rs
  • crates/ironclaw_host_runtime/Cargo.toml
  • crates/ironclaw_host_runtime/src/lib.rs
  • crates/ironclaw_host_runtime/src/sandbox_process.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/shell_limits.rs
  • crates/ironclaw_network/src/lib.rs
  • crates/ironclaw_network/src/policy.rs
  • crates/ironclaw_network/tests/network_policy_contract.rs

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs Outdated
Comment on lines +116 to +149
pub fn parse_host_pattern(raw: &str) -> Result<NetworkTargetPattern, NetworkPolicyError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(invalid_host_pattern(raw, "must not be empty"));
}
if trimmed == "*" {
return Err(invalid_host_pattern(
raw,
"bare `*` would match every host; use a specific hostname or a \
`*.`-prefixed wildcard label",
));
}

let label_source = trimmed.strip_prefix("*.").unwrap_or(trimmed);
let shape_ok = !label_source.is_empty()
&& !label_source.starts_with('.')
&& !label_source.ends_with('.')
&& !label_source.contains("..")
&& label_source
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-'));
if !shape_ok {
return Err(invalid_host_pattern(
raw,
"must be a valid hostname or a `*.`-prefixed wildcard label",
));
}

Ok(NetworkTargetPattern {
scheme: None,
host_pattern: trimmed.to_string(),
port: None,
})
}

@coderabbitai coderabbitai Bot Jul 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Missing length bound — this validator is not strictly stricter than validate_declaration.

validate_network_host_pattern (crates/ironclaw_host_api/src/action.rs, Line 105) caps host patterns at 253 bytes; parse_host_pattern has no cap. The doc claims this is the stricter chokepoint for untrusted operator input, and sandbox_network_policy builds NetworkTargetPattern from these strings without calling validate_declaration, so an arbitrarily long env-supplied "hostname" lands in allowed_targets unbounded. Per the crate's bounds rule for user-controlled strings, add the DNS cap here.

🔒️ Proposed fix
     if trimmed.is_empty() {
         return Err(invalid_host_pattern(raw, "must not be empty"));
     }
+    if trimmed.len() > 253 {
+        return Err(invalid_host_pattern(
+            raw,
+            "must be at most 253 bytes",
+        ));
+    }
     if trimmed == "*" {

As per coding guidelines: "Apply explicit bounds to user-controlled files, bodies, strings, collections".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn parse_host_pattern(raw: &str) -> Result<NetworkTargetPattern, NetworkPolicyError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(invalid_host_pattern(raw, "must not be empty"));
}
if trimmed == "*" {
return Err(invalid_host_pattern(
raw,
"bare `*` would match every host; use a specific hostname or a \
`*.`-prefixed wildcard label",
));
}
let label_source = trimmed.strip_prefix("*.").unwrap_or(trimmed);
let shape_ok = !label_source.is_empty()
&& !label_source.starts_with('.')
&& !label_source.ends_with('.')
&& !label_source.contains("..")
&& label_source
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-'));
if !shape_ok {
return Err(invalid_host_pattern(
raw,
"must be a valid hostname or a `*.`-prefixed wildcard label",
));
}
Ok(NetworkTargetPattern {
scheme: None,
host_pattern: trimmed.to_string(),
port: None,
})
}
pub fn parse_host_pattern(raw: &str) -> Result<NetworkTargetPattern, NetworkPolicyError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(invalid_host_pattern(raw, "must not be empty"));
}
if trimmed.len() > 253 {
return Err(invalid_host_pattern(raw, "must be at most 253 bytes"));
}
if trimmed == "*" {
return Err(invalid_host_pattern(
raw,
"bare `*` would match every host; use a specific hostname or a \
`*.`-prefixed wildcard label",
));
}
let label_source = trimmed.strip_prefix("*.").unwrap_or(trimmed);
let shape_ok = !label_source.is_empty()
&& !label_source.starts_with('.')
&& !label_source.ends_with('.')
&& !label_source.contains("..")
&& label_source
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-'));
if !shape_ok {
return Err(invalid_host_pattern(
raw,
"must be a valid hostname or a `*.`-prefixed wildcard label",
));
}
Ok(NetworkTargetPattern {
scheme: None,
host_pattern: trimmed.to_string(),
port: None,
})
}
🤖 Prompt for 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.

In `@crates/ironclaw_network/src/policy.rs` around lines 116 - 149, Update
parse_host_pattern to reject trimmed host patterns exceeding the existing
253-byte DNS limit before constructing NetworkTargetPattern. Preserve the
current empty, wildcard, and shape validation behavior, and use
invalid_host_pattern with an appropriate length-bound error.

Source: Coding guidelines

✅ Addressed in commits 103410a to 0056083

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 103410a: parse_host_pattern had no length cap while validate_declaration (action.rs:105) caps at 253 bytes, so the "stricter" claim was false on that axis. Full axis-by-axis comparison (validate_declaration vs parse_host_pattern): length — 253-byte cap vs none (FIXED, now matches); bare * — accepted vs rejected (intentional divergence per both docs, not collapsed); charset — alphanumeric+.-_ vs alphanumeric+.- (parse_host_pattern correctly stricter, no underscore); empty/leading-dot/trailing-dot/..— identical checks in both; control chars/NUL — explicit scan in validate_declaration, implicit via charset allowlist in parse_host_pattern (equivalent coverage); uppercase — both permissive, equivalent; IDN/punycode — neither decodes, both restrict to ASCII (equivalent). One additional minor divergence found and left as-is (not a strictness violation): parse_host_pattern trims surrounding whitespace before validating (pinned byparse_host_pattern_trims_surrounding_whitespace) where validate_declaration rejects a plain leading/trailing space outright via its charset scan — this normalizes rather than admits anything that resolves differently, so it doesn't reopen the "not strictly stricter" claim the length gap did. Added parse_host_pattern_rejects_patterns_over_253_bytes(RED before the fix) andparse_host_pattern_accepts_pattern_at_exactly_253_bytes` to pin the boundary. Not resolving — leaving for reviewer confirmation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A18 -B4 \
  'fn parse_host_pattern_(rejects_patterns_over_253_bytes|accepts_pattern_at_exactly_253_bytes)' \
  crates/ironclaw_network/tests crates/ironclaw_network/src

rg -n -A8 -B5 \
  'trimmed\.len\(\) > 253|must be at most 253 bytes' \
  crates/ironclaw_network/src/policy.rs

Length of output: 4559


@henrypark133 Confirmed. parse_host_pattern now applies the 253-byte cap before constructing NetworkTargetPattern, and the contract tests pin both sides of the boundary (253 accepted; >253 rejected).

This resolves the bounded-untrusted-input concern under the repository’s network/sandbox input-bounds invariant.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

`connect_override` embedded the full `IRONCLAW_REBORN_DOCKER_HOST` value
and Bollard's raw error into `RuntimeProcessError`. An earlier commit
sanitized `SandboxDockerReadiness.reason`, but that fixed one surface —
the underlying error still reached debug logs and every retry consumer
unchanged. Sanitizing per-consumer leaves the trap for the next one.

All five constructors now emit a fixed `{env var} <unix socket|http
endpoint> <connect|ping> failed`: endpoint kind and failure category
only, never the raw host or backend text.

The real credential vector was not a Bollard branch. The non-loopback
rejection echoed the configured value verbatim, and that is exactly
where a `user:pass@host` URL lands — user-info always fails the loopback
literal check. `hunter2` appeared in the error text pre-fix.

Detail is not rerouted to `debug!` either: this crate bans raw host
paths and backend errors in errors, events, snapshots, logs, and docs
with no debug carve-out, and an operator already knows the value they
configured. connect-vs-ping is the actionable signal.

Three regression tests, each proven RED against the pre-fix code: the
http user-info case, the unix-socket-path case, and the rejection branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 18:36
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 28, 2026 18:37 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

Sanitized at the source in connect_override: all five error constructors now emit fixed {env var} <unix socket|http endpoint> <connect|ping> failed — no raw host, no Bollard text — rather than fixing it per-consumer.

Also caught the actual credential vector, which was not a Bollard branch: the non-loopback rejection echoed the configured value verbatim, and that is exactly where a user:pass@host URL lands, since user-info always fails the loopback-literal check. hunter2 appeared in the error text pre-fix.

Three regression tests, each proven RED against pre-fix code (http user-info, unix socket path, rejection branch). See 0aa87eb.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_host_runtime/src/sandbox_process/connect.rs (1)

183-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Classify the Bollard connection cause instead of dropping it.

connect_override still uses .map_err(|_e| ...) at all four Bollard connect/ping branches with no bound-source logging. Per the crates/**/*.rs invariant, .map_err(|_| OtherError) may not discard the original cause. Map a fixed, host-free bollard::errors::Error source category to a stable failure class rather than preserving raw backend Display text.

🤖 Prompt for 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.

In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs` around lines 183
- 218, Update connect_override’s four Bollard connect and ping map_err handlers
to classify the original bollard::errors::Error into a fixed, host-free failure
category. Preserve the underlying cause through the project’s established error
representation without exposing raw backend Display text, and use distinct
stable categories for socket connect, socket ping, HTTP connect, and HTTP ping
failures.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@crates/ironclaw_host_runtime/src/sandbox_process/connect.rs`:
- Around line 183-218: Update connect_override’s four Bollard connect and ping
map_err handlers to classify the original bollard::errors::Error into a fixed,
host-free failure category. Preserve the underlying cause through the project’s
established error representation without exposing raw backend Display text, and
use distinct stable categories for socket connect, socket ping, HTTP connect,
and HTTP ping failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 96551c6c-50ed-44bc-897d-dd995787a7c2

📥 Commits

Reviewing files that changed from the base of the PR and between 6987a94 and 0aa87eb.

📒 Files selected for processing (1)
  • crates/ironclaw_host_runtime/src/sandbox_process/connect.rs

@henrypark133

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

@ironloopai

ironloopai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #6746

🔴 Failed

Execution result is invalid

The structured result could not be verified.

Manual command by @henrypark133 · attempt 1 of 3 · failed after 2m 15s

Failure details
  • Repository: nearai/ironclaw
  • Base: main at 7086872
  • Head: sandbox/slice1-connect-allowlist-limits at 0aa87eb
  • Created: Jul 28, 2026, 7:44 PM UTC
  • Updated: Jul 28, 2026, 7:46 PM UTC
  • Run: 16adcdc5-26c8-4370-98b3-d22998cdafe2
  • Latest attempt: 1 · Completed · ac778991-4ec0-413b-a4a7-4d22a9f4ae28
  • Failed during: Verification
  • Retryable: No
  • Failure: ee08f9b1-5d56-4129-962f-d53ee60f77a6

henrypark133 and others added 2 commits July 28, 2026 17:06
parse_host_pattern is documented as the stricter chokepoint relative to
NetworkTargetPattern::validate_declaration, but had no length cap while
validate_declaration caps at 253 bytes (the DNS name length limit). A
validator billed as strictly stricter must not be laxer on any axis —
an unbounded parse_host_pattern let an operator-supplied env var push
an arbitrarily large string into NetworkPolicy::allowed_targets that
validate_declaration would have rejected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ox allowlist

sandbox_extra_allowed_domains downgraded the NetworkTargetPattern that
parse_host_pattern already validated into a bare String, and
sandbox_network_policy later rebuilt the same struct field-by-field.
That round-trip discards the proof of validation and duplicates
construction logic — the same "type carries the proof" discipline
applied elsewhere in this stack. Both functions now thread
NetworkTargetPattern through directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 00:06
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 29, 2026 00:06 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

@ironloopai

ironloopai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #6746

🔴 Failed

Execution result is invalid

The structured result could not be verified.

Manual command by @henrypark133 · attempt 1 of 3 · failed after 1m 51s

Failure details
  • Repository: nearai/ironclaw
  • Base: main at 0f4907c
  • Head: sandbox/slice1-connect-allowlist-limits at 0056083
  • Created: Jul 29, 2026, 12:09 AM UTC
  • Updated: Jul 29, 2026, 12:11 AM UTC
  • Run: 064319c1-e6e3-44d0-b09f-8e53ab5561d0
  • Latest attempt: 1 · Completed · b20a9630-fcdd-4d85-95cb-9fdc864e08c3
  • Failed during: Verification
  • Retryable: No
  • Failure: bedb034c-a8ed-4b65-8ec9-941ad2f7d3ac

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_network/src/policy.rs (1)

72-79: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Preserve the integer parse failure.

Line 76 discards ParseIntError, leaving only a generic reason. Add a parse-specific typed error/source chain while retaining the sanitized operator-facing message.

This violates the Fail loud invariant. As per coding guidelines: “Do not use .map_err(|_| OtherError) when it discards the original cause.” As per path instructions: “Errors propagate with ? into thiserror types with context.”

🤖 Prompt for 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.

In `@crates/ironclaw_network/src/policy.rs` around lines 72 - 79, Update
parse_egress_limit’s trimmed.parse error mapping to retain the original
ParseIntError through a typed NetworkPolicyError source/error field while
preserving the sanitized operator-facing reason message. Ensure the parsing
failure propagates with its source chain instead of discarding the cause via
map_err(|_| ...).

Sources: Coding guidelines, Path instructions

🤖 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.

Outside diff comments:
In `@crates/ironclaw_network/src/policy.rs`:
- Around line 72-79: Update parse_egress_limit’s trimmed.parse error mapping to
retain the original ParseIntError through a typed NetworkPolicyError
source/error field while preserving the sanitized operator-facing reason
message. Ensure the parsing failure propagates with its source chain instead of
discarding the cause via map_err(|_| ...).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 34ecdbab-8bd6-402c-86a4-d890ad9ebe48

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa87eb and 0056083.

📒 Files selected for processing (3)
  • crates/ironclaw_host_runtime/src/sandbox_process/network_allowlist.rs
  • crates/ironclaw_network/src/policy.rs
  • crates/ironclaw_network/tests/network_policy_contract.rs

sandbox_allowed_domains() hand-built NetworkTargetPattern for
DEFAULT_SANDBOX_ALLOWED_DOMAINS instead of routing through
parse_host_pattern the way sandbox_extra_allowed_domains() does for
operator-supplied extras — the round-trip-through-String fix in 0056083
covered extras but left defaults on the old hand-built path, so a future
bad literal in the const would ship unvalidated where an operator typo
would not. Extracts parse_host_pattern_s for both call sites.

Addresses coderabbitai review comment on PR #6746 (discussion_r3668046975,
second pass — first pass on extras was already fixed in 0056083).
Copilot AI review requested due to automatic review settings July 29, 2026 17:19
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6746 July 29, 2026 17:19 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@henrypark133
henrypark133 merged commit 635de0f into main Jul 29, 2026
65 checks passed
@henrypark133
henrypark133 deleted the sandbox/slice1-connect-allowlist-limits branch July 29, 2026 18:30
henrypark133 added a commit that referenced this pull request Jul 29, 2026
Both PRs added a module declaration to sandbox_process.rs at the same
position. Resolution keeps both, alphabetically ordered: shell_limits
(from #6746, now on main) before tls_intercept (this branch).

Cargo.lock and ironclaw_host_runtime/Cargo.toml auto-merged; both PRs
added disjoint dependencies.

Verified after resolution: 182 sandbox_process tests pass (up from 147 —
#6746's suite arrives with the merge), TLS escape-hatch gate 16/16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…limits (nearai#6746)

* feat(sandbox): unwired Docker-connect retry, egress allowlist, shell limits

First slice of the sandbox container transport, which has never reached
main. Three leaf modules with no dependency on any other unlanded piece:

- `connect`: bounded-retry Docker connect + `IRONCLAW_REBORN_DOCKER_HOST`
  override + a readiness probe for boot diagnostics. Retry exhaustion is
  a hard error; there is deliberately no unsandboxed-host fallback.
- `network_allowlist`: the default sandboxed-shell egress allowlist
  (package registries + source hosts) as a `NetworkPolicy`, plus the
  `IRONCLAW_SANDBOX_EXTRA_ALLOWED_DOMAINS` operator hook.
- `shell_limits`: the timeout / captured-output defaults, floors, and
  ceilings for `builtin.shell`.

All three ship unwired, following the `ca.rs` / `credential_firewall.rs`
convention: each module doc names its real future consumer, and no fake
caller is invented to silence a lint. The only behavior-adjacent change
is `sandbox_process`'s `DEFAULT_TIMEOUT`/`DEFAULT_MAX_OUTPUT_BYTES` now
reading `shell_limits`' constants instead of repeating the same two
literals — same values, so no behavior change. In particular the clamp
functions are NOT wired: the live transport still applies a
model-supplied `timeout_secs` with no ceiling, and closing that is a
behavior change that ships with its own test.

`reborn_extension_specificity`'s PATH_TERM_COLLISIONS gains two entries
for `network_allowlist.rs` — the allowlist names github.com and
api.github.com as code hosts for `git clone` / release-archive
workflows, not as the github extension.

Crate-tier tests only, which is correct while these are unwired; the
integration-tier coverage is owed by the PR that gives each module a
production caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): address code review on the slice-1 leaf modules

- `network_allowlist`'s env read went through a bare `std::env::var` while
  the sibling `connect` — added in the same PR, which is also what pulls
  `ironclaw_common` in — went through `env_or_override`. One convention,
  not two: both now use `env_or_override`.
- The extras test mutated process-global env with raw `unsafe set_var` and
  no lock, next to `connect` tests that hold `lock_env()` across their
  whole set/read window for exactly that reason. It could interleave with
  them. Now uses `lock_env` + `set_runtime_env`/`remove_runtime_env`.
- That test also only asserted the parsed extras, so a regression that
  made operator extras *replace* the defaults instead of appending to them
  would have passed. It now asserts `sandbox_allowed_domains()` while the
  override is still set, and a new case pins the unset-env path.
- `connect_override`'s http/tcp branch — the one CI runners and DinD
  actually take — had no test. Added one pinning that its failure is still
  attributed to `IRONCLAW_REBORN_DOCKER_HOST`.
- That branch is plaintext and unauthenticated, with no `connect_with_ssl`
  path, while the module doc invited "remote daemons". Since this reaches
  an API that creates containers and mounts host paths, the constant now
  says plainly: unix socket or loopback only, TLS is not implemented here.
- Both new env vars documented in `.env.example`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(host_runtime): stop leaking raw Docker errors into sandbox readiness reason

sandbox_docker_readiness()'s public `reason` field embedded the raw
RuntimeProcessError, which connect_override formats with the
configured socket path / remote address plus Bollard's backend error
verbatim. The type's own doc comment says this is meant for a startup
log line or health endpoint — a surface that isn't necessarily
trusted-internal-only — so it shouldn't carry that detail. `reason` is
now a fixed string; the full error goes to a debug! log instead (info!
would corrupt the REPL/TUI per this repo's logging rule).

Extends readiness_surfaces_reason_on_unreachable_daemon to force a
deterministic unreachable endpoint via the env override and assert the
reason no longer contains it — verified this new assertion fails
against the prior implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(host_runtime): bound Docker connect retry to the doc-promised few seconds

connect_docker_with_retry's doc comment promised "a capped total wait
of a few seconds", but each attempt used a 120s Bollard client
timeout and MAX_ATTEMPTS=4, so a daemon that accepts a connection but
never answers ping() could block the caller for up to 8 minutes (the
250/500/1000ms backoff is noise against that). A Docker daemon that
hasn't answered ping() within a few seconds isn't healthy, so waiting
that long only delays the inevitable failure.

Introduces CONNECT_ATTEMPT_TIMEOUT (5s), used both as the Bollard
client-side timeout and to wrap each retry attempt in
tokio::time::timeout — the latter also bounds
Docker::connect_with_local_defaults(), which uses Bollard's own
internal timeout rather than a parameter this module controls. New
worst case: 4 * 5s + 1.75s cumulative backoff ~= 21.75s, documented on
connect_docker_with_retry.

Adds retry_worst_case_wall_clock_is_bounded, which pins the constants'
arithmetic rather than measuring real wall-clock time: a true
end-to-end timing test needs a Docker daemon that accepts the
connection but never pings back, which isn't reproducible without a
mock Docker client (rejected by design review for this module) and
would be flaky under CI scheduling jitter regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(network): hard-fail sandbox egress allowlist on invalid host patterns

sandbox_extra_allowed_domains() split IRONCLAW_SANDBOX_EXTRA_ALLOWED_DOMAINS
with zero shape validation, and host_matches_pattern treats a bare `*` as
"match every host" — one operator typo silently turned the sandboxed shell's
package-registry allowlist into allow-all egress for the one profile whose
entire purpose is holding untrusted code.

Add ironclaw_network::parse_host_pattern as the chokepoint: it rejects empty,
bare `*`, and anything that isn't a valid hostname or `*.`-prefixed wildcard
label. It is intentionally stricter than NetworkTargetPattern::
validate_declaration, which still permits a reviewed `*` for legitimate
full-access grants (DevWildcard, MCP full-network credential audiences) —
those are declared and reviewed, an env var typo is not.

sandbox_extra_allowed_domains/sandbox_allowed_domains/sandbox_network_policy
become fallible so a bad entry is a loud boot-time failure instead of a
silently narrowed allowlist nobody notices until someone audits traffic.
Both call sites are unwired today, so threading Result now is cheap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(host_runtime): give the sandboxed shell a concrete egress byte ceiling

network_allowlist.rs set max_egress_bytes: None on the sandboxed shell's
NetworkPolicy. None is the repo-wide convention, but this is the one profile
whose entire purpose is executing untrusted code, so an absent volume cap
matters more here than anywhere else it's used.

Add DEFAULT_SANDBOX_MAX_EGRESS_BYTES (2 GiB, per-request) with an
IRONCLAW_SANDBOX_MAX_EGRESS_BYTES env override, following the same
env_or_override precedence as the sibling extra-domains var. 2 GiB sits one
to two orders of magnitude above ordinary cargo/npm/pip/git payloads (so it
should not false-positive-block legitimate builds) while still bounding a
single request far short of a deliberate bulk-exfiltration attempt. The
override is validated (reject non-numeric/zero) through the same
ironclaw_network fail-loud path as the hostname-pattern validator, via the
new parse_egress_limit chokepoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(host_runtime): restrict Docker override to loopback unless opted in

connect_override's HTTP branch dialed any IRONCLAW_REBORN_DOCKER_HOST
address with no enforcement, while the module's own doc comment says safe
use is restricted to "a unix socket or a loopback address" and
.env.example advertised the var as usable for "remote daemons" with no
caveat. Plaintext, unauthenticated access to a remote Docker Engine API is
host-root-equivalent (it can create containers and mount host paths), so
docs and code disagreeing here is a real gap, not just stale wording.

Default to loopback-or-unix-socket, matching what the docstring already
promised. A non-loopback plaintext HTTP host now requires the separately
named IRONCLAW_REBORN_DOCKER_HOST_ALLOW_REMOTE=1 opt-in, mirroring the
SANDBOX_ALLOW_FULL_ACCESS "required second opt-in" pattern already used for
another dangerous default in .env.example. Bare "reject non-loopback"
would break the one legitimate DinD/CI sidecar case (a container-network
IP); RFC1918-only isn't a trust boundary. The rejection error names both
the disallowed host and the env var that unlocks it, so an operator
doesn't need to read Rust source to proceed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: cargo fmt fixes for fix 1/fix 2 sandbox network changes

Formatting-only, no behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sandbox): fix egress-cap and validator-divergence doc gaps

Two adversarial-review doc fixes so a future implementer doesn't wire a
control incorrectly:

1. DEFAULT_SANDBOX_MAX_EGRESS_BYTES's doc understated the gap between
   today's per-request enforcer (host-mediated, sees plaintext, computes
   estimated_bytes) and the not-yet-built CONNECT proxy this list feeds,
   which tunnels opaque TLS and structurally cannot size "one request"
   the same way. Rewrote the doc to say so precisely and added a
   TODO(security) so the proxy-wiring PR doesn't inherit "per-request"
   semantics by assumption.

2. validate_declaration (permissive, action.rs) and parse_host_pattern
   (strict, policy.rs) diverge intentionally but only one side explained
   why, inviting a future "fix this inconsistency" PR to tighten the
   wrong one and break DevWildcard's declared full-access grant. Added a
   reciprocal cross-reference on both sides. Also corrected an inaccurate
   claim in the existing doc: "MCP full-network credential audiences" is
   not a real consumer of the bare-`*` allowance — extension manifests
   already reject a wildcard audience host via
   ManifestV3Error::WildcardAudienceHost. CapabilityNetworkProfile::
   DevWildcard is the only confirmed production consumer.

No code or values changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): bound sandbox_docker_readiness inside connect_once

sandbox_docker_readiness() called connect_once() directly, which never
wrapped the local-default discovery branch in CONNECT_ATTEMPT_TIMEOUT.
Docker::connect_with_local_defaults() carries its own ~120s internal
client timeout, so a daemon that accepts the connection but never
answers ping() could stall the probe far past the "cheap one-shot"
bound SandboxDockerReadiness promises.

Move the timeout into connect_once itself (one chokepoint for both
callers) instead of adding a second wrapper at the readiness call
site. connect_docker_with_retry's per-attempt wrap becomes redundant
and is dropped; its documented worst-case arithmetic (~21.75s) is
unchanged since each attempt is still capped at CONNECT_ATTEMPT_TIMEOUT.

Adds a stalled-future regression test: a real unix-socket listener
that accepts the connection and never answers, with real DOCKER_HOST
pointed at it so the local-default branch (the one with no
parameterized timeout) is exercised. Confirmed it fails against the
pre-fix shape (elapsed ~12s past a 12s ceiling) before confirming it
passes against the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): sanitize Docker connect errors at their source

`connect_override` embedded the full `IRONCLAW_REBORN_DOCKER_HOST` value
and Bollard's raw error into `RuntimeProcessError`. An earlier commit
sanitized `SandboxDockerReadiness.reason`, but that fixed one surface —
the underlying error still reached debug logs and every retry consumer
unchanged. Sanitizing per-consumer leaves the trap for the next one.

All five constructors now emit a fixed `{env var} <unix socket|http
endpoint> <connect|ping> failed`: endpoint kind and failure category
only, never the raw host or backend text.

The real credential vector was not a Bollard branch. The non-loopback
rejection echoed the configured value verbatim, and that is exactly
where a `user:pass@host` URL lands — user-info always fails the loopback
literal check. `hunter2` appeared in the error text pre-fix.

Detail is not rerouted to `debug!` either: this crate bans raw host
paths and backend errors in errors, events, snapshots, logs, and docs
with no debug carve-out, and an operator already knows the value they
configured. connect-vs-ping is the actionable signal.

Three regression tests, each proven RED against the pre-fix code: the
http user-info case, the unix-socket-path case, and the rejection branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(network): bound parse_host_pattern to 253 bytes

parse_host_pattern is documented as the stricter chokepoint relative to
NetworkTargetPattern::validate_declaration, but had no length cap while
validate_declaration caps at 253 bytes (the DNS name length limit). A
validator billed as strictly stricter must not be laxer on any axis —
an unbounded parse_host_pattern let an operator-supplied env var push
an arbitrarily large string into NetworkPolicy::allowed_targets that
validate_declaration would have rejected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(host_runtime): carry validated NetworkTargetPattern through sandbox allowlist

sandbox_extra_allowed_domains downgraded the NetworkTargetPattern that
parse_host_pattern already validated into a bare String, and
sandbox_network_policy later rebuilt the same struct field-by-field.
That round-trip discards the proof of validation and duplicates
construction logic — the same "type carries the proof" discipline
applied elsewhere in this stack. Both functions now thread
NetworkTargetPattern through directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sandbox): route default sandbox domains through parse_host_pattern

sandbox_allowed_domains() hand-built NetworkTargetPattern for
DEFAULT_SANDBOX_ALLOWED_DOMAINS instead of routing through
parse_host_pattern the way sandbox_extra_allowed_domains() does for
operator-supplied extras — the round-trip-through-String fix in 0056083
covered extras but left defaults on the old hand-built path, so a future
bad literal in the const would ship unvalidated where an operator typo
would not. Extracts parse_host_pattern_s for both call sites.

Addresses coderabbitai review comment on PR nearai#6746 (discussion_r3668046975,
second pass — first pass on extras was already fixed in 0056083).

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6746 — 44a0a580 Deployed Jul 29, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: dependencies Dependency updates size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants