Skip to content

feat(sandbox): leaf-scoped mount containment + per-user sandbox identity primitives - #6695

Merged
henrypark133 merged 11 commits into
mainfrom
sandbox/pr3-leaf-scoped-containment
Jul 28, 2026
Merged

henrypark133 merged 11 commits into
mainfrom
sandbox/pr3-leaf-scoped-containment

Conversation

@henrypark133

Copy link
Copy Markdown
Collaborator

Summary

Two related, unwired-by-design slices toward the persistent per-user sandbox
container program:

  • ironclaw_filesystem: leaf-scoped mount containment. resolve_joined
    now returns a per-request containment_root that, for a leaf_scoped
    mount, is host_root/<first-tail-segment> instead of the shared
    host_root — closing a same-mount cross-leaf symlink escape that a plain
    mount_local containment check (host_root only) would not catch. A
    bare-mount-root request against such a mount is rejected outright (there is
    no safe containment root for "every caller's leaf"). mount_local_per_leaf
    is the constructor.
  • ironclaw_host_runtime: per-user sandbox identity + attribution
    primitives.
    RebornSandboxUserKey (a {tenant, user}-only container/
    workspace key — every thread/project/agent for the same user shares one
    container), the labels-as-identity registry module (Docker label
    helpers, SandboxActivityRegistry, BackgroundJobRegistry), and
    ConnectionAttributionResolver (source-IP → {tenant, user} resolution
    for the shared sandbox egress proxy, design decision D9 — fail-closed on
    any ambiguity: duplicate IP, missing/malformed labels, or a query error
    all collapse to Unattributed, never a guess).

Scope limits — read before reviewing

  • The two leaf_scoped containment tests
    (leaf_scoped_mount_rejects_bare_mount_root_request,
    leaf_scoped_mount_rejects_cross_leaf_symlink_escape) prove the boundary
    at unit tier only.
    sandbox_cross_tenant_escape.rs — the composition-tier
    test that drives the actual end-to-end cross-tenant attack through a real
    Docker container — is NOT included in this PR. Its imports
    (RebornSandboxConfig, RebornScopedSandboxCommandTransport,
    CommandExecutionRequest) resolve to the full exec-based sandbox
    transport (exec_transport.rs, connect.rs, egress_proxy.rs,
    reaper.rs — roughly 4500 new lines), which is not on main and is far
    outside a reviewable PR size. The end-to-end cross-tenant read/write
    escape is therefore not proven by this PR
    — it ships with the transport
    slice later, driven through the same harness.
  • Every new item here lands unwired by design. mount_local_per_leaf,
    RebornSandboxUserKey, SandboxActivityRegistry, and the registry helpers
    have no production caller on main today — their consumers are the
    exec-based transport's per-user container reuse and Task A5's reaper
    (exec_transport, not in this PR). ConnectionAttributionResolver is
    consumed by W6 (egress-proxy TLS termination + credential injection), also
    not built yet. This is deliberate: the sandbox program lands skeleton
    pieces unwired and profile-gates them later, to avoid stacked-PR drift.
    Every such item is pub/re-exported (no dead-code lint fires) or carries
    a targeted #[allow(dead_code)] naming its real future consumer — no
    #[allow] was added without a named consumer, and no fake caller was
    invented to silence a lint.
  • Ordinary (non-leaf-scoped) mount_local behavior is unchanged:
    containment_root is only mutated if mount.leaf_scoped && index == 0,
    so it stays equal to host_root on every existing path. Pre-existing
    TOCTOU between canonicalize and use is unchanged and out of scope.

Fix applied after review

attribution's real-Docker test originally gated on docker_gate:: docker_available() (which shells out to the docker CLI — context-aware:
Colima, Docker Desktop, a remote host) but then connected directly via
Docker::connect_with_local_defaults(), which only honors DOCKER_HOST or
the hardcoded /var/run/docker.sock. That let the gate pass while the
connection still failed on any non-default-socket machine (reproduced
locally on a Colima-backed dev machine). Fixed by reusing
sandbox_process::connect_docker() — the same connect_with_local_defaults()
→ unix_socket_candidates() (~/.colima/default/docker.sock,
~/.rd/docker.sock, etc.) fallback production containers already connect
through — instead of reimplementing resolution. A connect_docker() failure
now prints a SKIP: line rather than panicking, since even that broader
fallback can't cover every possible Docker context.

Verified with DOCKER_HOST unset: the test now genuinely passes (not a
skip) via the Colima socket fallback.

Test plan

  • cargo fmt --all --check — clean
  • cargo clippy -p ironclaw_filesystem --tests — 0 warnings
  • cargo clippy -p ironclaw_host_runtime --tests — 0 warnings
  • IRONCLAW_DISABLE_OS_KEYCHAIN=1 cargo test -p ironclaw_filesystem — all pass, including the two new leaf-containment tests
  • IRONCLAW_DISABLE_OS_KEYCHAIN=1 cargo test -p ironclaw_host_runtime --lib — 412 passed; 5 pre-existing failures unrelated to this diff (3 in sandbox_process.rs's existing test module that hardcode /var/run/docker.sock, unaffected by any file this PR touches; 2 in first_party_tools::trace_commons, unrelated). None of these 5 are new or introduced by this PR.

🤖 Generated with Claude Code

henrypark133 and others added 2 commits July 26, 2026 22:15
…ity primitives

Ships two related, unwired-by-design slices of the persistent per-user
sandbox program:

- ironclaw_filesystem: leaf-scoped mount containment. resolve_joined now
  returns a per-request containment_root that, for a leaf_scoped mount, is
  host_root/<first-tail-segment> instead of the shared host_root — closing
  a same-mount cross-leaf symlink escape a plain mount_local containment
  check would miss. mount_local_per_leaf is the constructor; a bare-root
  request against such a mount is rejected outright (no safe containment
  root for "every caller's leaf").

- ironclaw_host_runtime: identity + attribution primitives for the
  persistent per-user sandbox container model — RebornSandboxUserKey
  ({tenant,user}-only container/workspace key), the labels-as-identity
  registry (Docker label helpers, SandboxActivityRegistry,
  BackgroundJobRegistry), and ConnectionAttributionResolver (source-IP to
  {tenant,user} resolution for the shared egress proxy, design decision D9).

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

docker_gate::docker_available() shells out to the docker CLI, which resolves the daemon through whatever context is active (Colima, Docker Desktop, a remote host). The test then connected directly via Docker::connect_with_local_defaults(), which only honors DOCKER_HOST or the hardcoded /var/run/docker.sock, so the gate could pass while the connection still failed on any machine using a non-default socket.

Reuse sandbox_process::connect_docker() instead of reimplementing resolution: it already tries connect_with_local_defaults() then falls back through unix_socket_candidates(), the same path production containers connect through. A connect_docker() failure now prints a SKIP line rather than panicking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 05:54
@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.

@ironloopai

ironloopai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: cd10cd5b216b42f7f1642b0da6ece9dac8d9ffe6
Result: One or more review results were superseded by a newer PR head.
Next: Run @ironloopai review on the latest PR head.
Updated: 2026-07-27T22:13:09.859Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Superseded N/A N/A 2026-07-27T22:13:09.846Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Superseded by a newer PR head. New head: cd10cd5. Previous verdict: Changes requested.
Recent activity
Time Reviewer State Detail
2026-07-27T21:34:16.565Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (b8650a4).
2026-07-27T21:36:58.815Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head b8650a4.
2026-07-27T21:36:58.815Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-27T21:36:59.710Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-27T21:37:02.065Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at a8429df.
2026-07-27T21:42:51.891Z ironloop/common-reviewer (reviewer) Result captured Changes requested; 1 blocking finding.
2026-07-27T21:42:51.891Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
2026-07-27T22:13:09.846Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (cd10cd5).
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

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-6695 July 27, 2026 05:54 Destroyed
@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 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 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: 6f80b243-37f9-46e7-a417-84dc5d2f1d00

📥 Commits

Reviewing files that changed from the base of the PR and between b8650a4 and cd10cd5.

📒 Files selected for processing (3)
  • crates/ironclaw_architecture/tests/reborn_struct_test_support_ratchet.rs
  • crates/ironclaw_host_runtime/src/lib.rs
  • crates/ironclaw_host_runtime/src/sandbox_process.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added mount_local_per_leaf for leaf-scoped local mounts with per-request containment handling.
    • Added sandbox {tenant,user} identity key helpers plus re-exported sandbox activity registry types.
    • Added peer IP connection attribution with TTL caching.
  • Bug Fixes

    • Fail-closed path containment for bare mount-root access.
    • Enforced per-leaf symlink escape prevention and dangling-final-symlink rejection on write paths.
    • Improved directory bootstrapping and reduced TOCTOU risk during write resolution.
  • Documentation

    • Updated filesystem and host-runtime adapter contract documentation.
  • Tests

    • Expanded leaf-scoped filesystem and Docker-gated attribution test coverage.

Walkthrough

The PR adds leaf-scoped filesystem mounts, per-user sandbox identity and registries, Docker network attribution with caching, Docker test gating, shared digest encoding, and public runtime re-exports.

Changes

Leaf-scoped filesystem containment

Layer / File(s) Summary
Leaf containment resolution and validation
crates/ironclaw_filesystem/src/local.rs, docs/reborn/contracts/filesystem.md
Adds leaf-specific containment roots, bootstrap handling for new leaves, bare-root rejection, cross-leaf symlink protection, dangling-symlink rejection, and async contract tests.

Sandbox runtime foundations

Layer / File(s) Summary
Per-user identity and container registries
crates/ironclaw_host_runtime/src/sandbox_process/{key_codec.rs,user_key.rs,registry.rs}, crates/ironclaw_host_runtime/src/{sandbox_process.rs,lib.rs}
Adds digest-backed tenant/user keys, container labels, activity and background-job registries, module wiring, and public re-exports.
Shared scope identity codec
crates/ironclaw_host_runtime/src/sandbox_process/scope_key.rs
Migrates scope-key encoding and digest generation to shared codec helpers.
Docker network connection attribution
crates/ironclaw_host_runtime/src/sandbox_process/{attribution.rs,attribution_tests.rs}, crates/ironclaw_host_runtime/tests/support/docker_gate.rs
Resolves unique Docker network IP matches to validated tenant/user labels with TTL caching, invalidation, fail-closed handling, concurrency tests, and gated integration coverage.
Runtime contract and architecture support
docs/reborn/contracts/host-runtime.md, crates/ironclaw_architecture/tests/reborn_struct_test_support_ratchet.rs
Updates scope-derived identity documentation and frozen architecture counts.

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

Sequence Diagram(s)

sequenceDiagram
  participant Proxy
  participant ConnectionAttributionResolver
  participant Docker
  participant ContainerLabels

  Proxy->>ConnectionAttributionResolver: resolve(peer_ip)
  ConnectionAttributionResolver->>Docker: containers_on_network(network)
  Docker-->>ConnectionAttributionResolver: container summaries and network IPs
  ConnectionAttributionResolver->>ContainerLabels: validate tenant/user labels
  ContainerLabels-->>ConnectionAttributionResolver: Attributed or Unattributed
  ConnectionAttributionResolver-->>Proxy: attribution result
Loading

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#6673 — Directly related architecture-ratchet changes for the attribution members added here.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and testing, but omits most required template sections, including Change Type, Linked Issue, Security Impact, and Rollback Plan. Fill in the missing template sections: Change Type, Linked Issue, Validation, full Test Strategy, Security Impact, Blast Radius, Rollback Plan, and Review Follow-Through.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is Conventional Commits-style and accurately summarizes the sandbox filesystem and per-user identity 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.

@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: 433c97a0b7

ℹ️ 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 thread crates/ironclaw_host_runtime/src/sandbox_process/user_key.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs Outdated

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

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 2 0 2 433c97a0b7a0

Head: 433c97a0b7a010dd8d73d2c5d51f5a849e8bc400
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Changes requested: the new sandbox backend drops per-request mount scope, and attribution can return a stale tenant/user after Docker IP reuse.

Findings

Blocking: 2 / Notes: 0

Blocking findings

1. ❌ [HIGH] Derive Docker mount roots from the request scope

Location: crates/ironclaw_process_sandbox/src/docker.rs:485
ProcessSandboxExecutor drops ProcessExecutionRequest.mounts, and this backend binds the static configuration roots for every request. A singleton executor therefore gives distinct tenant/project scopes the same workspace, tools, and cache directories, allowing cross-scope reads and writes. Resolve trusted host roots from the request's scoped mount authority and add a two-scope isolation test.

2. ❌ [HIGH] Do not cache attribution across container lifetimes

Location: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:188
This returns a positive IP-only cache entry without checking whether Docker has recycled that IP. Teardown invalidation is not wired, so a new user's connection within the five-second TTL can be attributed to the previous user and receive that user's injected egress credential. Make attribution cache-safe across container replacement (or invalidate synchronously on teardown) and add an IP-reuse regression test.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.
Inline review fallback

Inline comment projection fell back to a body-only PR Review because GitHub rejected the inline payload.
Reason: Unprocessable Entity: "Path could not be resolved" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request

IronLoop preserved the inline review comment payloads below instead of dropping them.

Inline fallback 1: crates/ironclaw_process_sandbox/src/docker.rs:485

ProcessExecutionRequest.mounts is dropped before reaching this backend, and all invocations bind the static config roots here. Since the host holds one ProcessExecutor, requests from distinct tenant/project scopes will share workspace/tools/cache directories. Resolve trusted roots per request from scoped mount authority and add an A/B scope-isolation test.

Inline fallback 2: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:188

An IP-only cache can return a stale identity after Docker reuses an IP. No teardown invalidation is wired, so a new user's connection within five seconds is returned as the previous user and would receive that user's egress credentials. Do not cache positive attribution across container lifetimes, or make invalidation synchronous and automatic.

@henrypark133 henrypark133 left a comment

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.

Code Review (multi-agent)

Intent: Add leaf-scoped mount containment and per-user sandbox identity primitives while deferring production wiring and Docker escape coverage.

Stats: 9 findings (from 9 raw, 9 after overlap/same-line dedup) across 5 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach (completed sequentially in the parent after platform parallel-agent cap). Reviewers failed: none; parallel fan-out was unavailable after the intent/partial lanes due to the active-agent thread cap. Body-only: 0.

The two High findings need resolution before the deferred transport/composition slices rely on these primitives. The remaining findings are coverage, bounded-cache, or maintainability follow-ups.

Security

  1. High IP-only attribution cache can cross-attribute a recycled Docker IP (crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:135-138, confidence 92) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:136
    The resolver caches an attributed {tenant,user} solely by peer IP and accepts that value for up to five seconds. If a container is torn down and Docker assigns the same IP to another tenant during that window, the next connection from the new container is attributed to the old owner. The module explicitly acknowledges this path, but the consumer is intended to choose credentials from this result, so a bounded stale window is still a cross-tenant credential-confusion vulnerability rather than fail-closed behavior.

Bugs

  1. High Leaf-scoped create operations reject a brand-new leaf (crates/ironclaw_filesystem/src/local.rs:188-195, confidence 95) — anchor: crates/ironclaw_filesystem/src/local.rs:188; also flagged by tests/Medium
    For a path such as /tmp/<new-user>/file, ensure_existing_ancestor_contained walks up to the existing shared host_root because the leaf does not exist yet, then checks that ancestor against host_root/<leaf>. That check necessarily fails, so resolve_for_create_dir_all and the missing-file branch of resolve_for_write cannot create the first directory for a new user. The newly added tests only cover pre-existing leaves, leaving the normal first-use path broken.

Performance

  1. Medium Expired attribution entries are never evicted (crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:148-148, confidence 94) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:148
    The TTL only causes cache misses; expired entries remain in the HashMap indefinitely. Once the proxy wires this resolver, each previously unseen peer IP permanently retains a CacheEntry, so long-running container churn can grow the cache without bound.

Tests

  1. Medium Write-path leaf escape is not tested (crates/ironclaw_filesystem/src/local.rs:143-169, confidence 95) — anchor: crates/ironclaw_filesystem/src/local.rs:159
    The new leaf-scoped containment is exercised only through read_file. The distinct resolve_for_write path, including canonicalized-parent validation, has no test proving a cross-leaf symlink cannot redirect writes.
  2. Low Candidate parsing lacks malformed-summary coverage (crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:97-103, confidence 100) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:97
    UserContainerCandidate::from_summary has untested failure branches for a missing container ID and a malformed created_at label; the current test covers only missing labels.
  3. Low Malformed or missing network IPs are untested (crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:261-272, confidence 100) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:261
    container_ip_on_network explicitly handles absent network settings, absent network maps, missing network entries, empty strings, and unparseable IPs, but the attribution tests cover only valid IPs and an unknown valid IP.
  4. Low Background job registry has no behavior tests (crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:183-217, confidence 100) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:183
    The new BackgroundJobRegistry is entirely untested, leaving record, per-user isolation, empty lookup, and drop_dead filtering behavior uncovered.
  5. Low Activity registry mutex contention is untested (crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:135-159, confidence 95) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:139
    SandboxActivityRegistry uses a shared Mutex and is intended for concurrent exec transport and reaper access, but existing tests are entirely sequential and do not exercise concurrent touch/read/forget operations.

Maintainability

  1. Low Remove temporary PR-state details from module comments (crates/ironclaw_host_runtime/src/sandbox_process.rs:37-46, confidence 85) — anchor: crates/ironclaw_host_runtime/src/sandbox_process.rs:37
    This module comment records the PR's sequencing, omitted future files, and anticipated consumers rather than stable code behavior. It will become stale as the follow-up transport wiring lands and duplicates the PR description, making the module header noisier to navigate.

Comment thread crates/ironclaw_filesystem/src/local.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Comment thread crates/ironclaw_filesystem/src/local.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process.rs Outdated

@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: 4

🤖 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_filesystem/src/local.rs`:
- Around line 187-201: Update ensure_existing_ancestor_contained and the
create-directory flow around resolve_joined so the shared mount root is accepted
as a bootstrap ancestor when the target leaf is absent. Run create_dir_all
first, then canonicalize the result and enforce containment with
ensure_contained against the leaf boundary. Add a caller-level regression test
covering creation of a previously absent leaf directory.

In `@crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs`:
- Around line 250-253: Add a tracing::debug! call in the
parse_attribution_labels None branch within the surrounding attribution method
before returning ConnectionAttribution::Unattributed, including enough context
to identify the container or label parsing failure while preserving the existing
fail-closed behavior.

In `@crates/ironclaw_host_runtime/src/sandbox_process/registry.rs`:
- Around line 175-218: Add unit tests for BackgroundJobRegistry covering record
and jobs_for storage/retrieval, including isolation by RebornSandboxUserKey, and
drop_dead retaining only jobs whose PIDs appear in alive_pids. Follow the direct
registry-test style used for SandboxActivityRegistry, without adding production
callers or unrelated refactoring.
- Line 22: Replace the sibling-module super imports with crate-rooted imports to
follow the repository convention: update registry.rs lines 22-22 for
RebornSandboxUserKey and attribution.rs lines 62-62 for label_tenant and
label_user, using crate::sandbox_process paths in both locations.
🪄 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: 11885cc2-e70a-4357-a4a9-611dfb5999af

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb8b1a and 433c97a.

📒 Files selected for processing (7)
  • crates/ironclaw_filesystem/src/local.rs
  • crates/ironclaw_host_runtime/src/lib.rs
  • crates/ironclaw_host_runtime/src/sandbox_process.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/user_key.rs
  • crates/ironclaw_host_runtime/tests/support/docker_gate.rs

Comment thread crates/ironclaw_filesystem/src/local.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
@railway-app

railway-app Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 27, 2026 at 10:24 pm

…ene, docker CI gate

Leaf-scoped mounts rejected a brand-new leaf's first write/create_dir_all
(ensure_existing_ancestor_contained had no bootstrap case for the shared
host_root when a caller's leaf doesn't exist yet); accept that one ancestor
now and add regression coverage for write-path creation, write-path
cross-leaf symlink escape, and create_dir_all bootstrap.

Attribution cache: sweep expired entries on miss so a long-running resolver
doesn't grow the cache unboundedly, and log the missing/malformed-label
fail-closed branch like its sibling branches. Add coverage for malformed/
missing container network IPs.

Docker CI gate: the attribution real-Docker test's connect_docker() failure
branch always skipped, even under IRONCLAW_REQUIRE_DOCKER_TESTS=1 — panic
in that mode instead, matching docker_gate's existing fail-closed pattern.
Pull busybox:1.36 before create_container so the test doesn't depend on a
pre-warmed local image cache (this is what broke it in CI).

registry.rs/attribution.rs: crate::-rooted imports per repo convention;
trim sandbox_process.rs's module header to stable ownership, not PR-state.
Add BackgroundJobRegistry, malformed-candidate-parsing, and concurrent
SandboxActivityRegistry coverage.

docs/reborn/contracts/host-runtime.md: one forward-pointing sentence
noting RebornSandboxUserKey's future coarser identity model doesn't yet
supersede the scope-derived identity this contract already documents.

architecture ratchet: baseline the two new test/dead-code seams this
introduces (attribution.rs dead-code method x4, test-support method x1).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 06:24
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6695 July 27, 2026 06:24 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.

@github-actions github-actions Bot added the scope: docs Documentation label Jul 27, 2026
@henrypark133

Copy link
Copy Markdown
Collaborator Author

Addressing the one review finding with no inline thread (ironloop's review fell back to body-only because GitHub rejected its inline payload — "Path could not be resolved"):

"Derive Docker mount roots from the request scope" (crates/ironclaw_process_sandbox/src/docker.rs:485) — INVALID for this PR: that file isn't in this PR's diff at all (git diff --name-only against this PR touches only ironclaw_filesystem/local.rs and the six ironclaw_host_runtime/sandbox_process files). It's pre-existing code in a different crate, unrelated to the two slices this PR ships (leaf-scoped mount containment, per-user sandbox identity primitives). Not something to fix here; flagging for a separate issue if it's a live concern on ironclaw_process_sandbox.

ironloop's second blocking finding (attribution cache IP reuse, attribution.rs:188) duplicates the inline threads already replied to and resolved below, along with all 15 other inline findings (codex, coderabbitai, and my own code-review-skill run). Pushed in d17e92a: leaf-creation bootstrap fix + tests, attribution cache eviction + tests + trace log, docker-gate required-lane fix + busybox pull-before-create, crate::-rooted imports, module-comment trim, and one forward-pointing doc sentence for the RebornSandboxUserKey/contract-doc tension (per thermo-nuclear-code-quality-review ruling — see thread replies for both NEEDS-DISCUSSION items).

@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_filesystem/src/local.rs (1)

583-634: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not allow host_root bootstrap when containment_root already exists.

ensure_existing_ancestor_contained only matches the canonical ancestor against bootstrap_root, so a symlink inside an already-created leaf that points back to host_root can satisfy the exception and let create_dir_all(parent) create a directory outside containment_root before the later canonical check rejects the path. Clamp the bootstrap path to !containment_root.exists() and add a regression for this symlink case.

Violates crates/AGENTS.md backend containment invariant: backends must remain contained against symlink traversal, mount escape, and raw-host-path access.

🤖 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_filesystem/src/local.rs` around lines 583 - 634, The
bootstrap exception in ensure_existing_ancestor_contained must only apply when
containment_root does not already exist. Require the bootstrap check to be gated
by the absence of containment_root, preventing symlinks within an existing leaf
from redirecting directory creation to host_root. Add a regression test covering
an existing leaf containing a symlink to host_root and verifying creation
outside containment_root is rejected.

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_filesystem/src/local.rs`:
- Around line 583-634: The bootstrap exception in
ensure_existing_ancestor_contained must only apply when containment_root does
not already exist. Require the bootstrap check to be gated by the absence of
containment_root, preventing symlinks within an existing leaf from redirecting
directory creation to host_root. Add a regression test covering an existing leaf
containing a symlink to host_root and verifying creation outside
containment_root is rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 30ac60df-e3ae-4361-a964-5ef64f3dd2ac

📥 Commits

Reviewing files that changed from the base of the PR and between 433c97a and d17e92a.

📒 Files selected for processing (7)
  • crates/ironclaw_architecture/tests/reborn_struct_test_support_ratchet.rs
  • crates/ironclaw_filesystem/src/local.rs
  • crates/ironclaw_host_runtime/src/sandbox_process.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
  • crates/ironclaw_host_runtime/tests/support/docker_gate.rs
  • docs/reborn/contracts/host-runtime.md

@henrypark133 henrypark133 left a comment

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.

Code Review (multi-agent)

Intent: Introduce leaf-scoped filesystem mount containment and per-user sandbox identity primitives as intentionally unwired building blocks for a future persistent per-user sandbox container program.

Stats: 6 findings (from 6 raw, 4 after dedup) across 2 files. Reviewers run: security, performance, tests, conventions. Reviewers failed: bugs, local-patterns, maintainability, approach. Body-only: 0

Security

  1. High Cached IP attribution can inject credentials for the wrong user (crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:125-143, confidence 90) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:125
    The resolver caches an IP-to-user attribution for up to five seconds, but Docker reuses container IPs after teardown. If a new user's container receives the old IP before the TTL expires, requests from that container are attributed to the previous user and the future credential firewall can inject the wrong user's secret.
    Fix: Resolve attribution per accepted connection or bind the cache entry to a verified container identity and synchronously invalidate it before IP reuse.

Performance

  1. Medium Concurrent cache misses all query Docker independently (crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:187-205, confidence 91) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:187
    The cache is checked before the async Docker query and only locked afterward, so concurrent resolutions for uncached IPs each issue a full container-list request. During connection bursts this creates a thundering herd against Docker and repeatedly scans the entire network container list.
    Fix: Coalesce in-flight resolutions or add a short-lived shared snapshot so concurrent misses await one Docker listing.
  2. Medium Background job registry can grow without bound (crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:183-210, confidence 88) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:183
    Every background launch appends a String-bearing job to a per-user Vec, and jobs_for clones the entire vector. Until drop_dead runs successfully, repeated launches retain all entries and cause O(J) allocation/copying on every foreground lookup, allowing memory and latency to grow with job history.
    Fix: Bound retained jobs and command-preview sizes, and return a bounded snapshot or maintain only currently live jobs keyed by PID.
  3. Low Dead-job pruning is O(jobs x alive PIDs) (crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:213-216, confidence 84) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:213
    drop_dead calls alive_pids.contains for every retained job, making cleanup O(JxA). With many tracked background jobs and processes, the reaper repeatedly performs a linear PID scan for each job.
    Fix: Convert alive_pids to a HashSet before retain and use constant-time membership checks.

Tests

  1. Medium Empty alive PID lists are not tested (crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:213-216, confidence 90) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/registry.rs:213
    The collection parameter is tested with multiple live and dead PIDs, but not an empty list. An empty process listing should remove every tracked job; without coverage, stale jobs could remain when the sandbox has no live background processes.
    Fix: tests::registry::background_job_registry_drop_dead_with_empty_alive_pids_removes_all_jobs covering an empty alive_pids slice
  2. Low Concurrent attribution cache access is untested (crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:187-205, confidence 75) — anchor: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:187
    The resolver performs an async Docker query outside its Mutex and then reinserts the result, but no test invokes resolve concurrently for the same or different IPs. This leaves the cache miss, query, sweep, and insert interaction under contention unexercised.
    Fix: tests::attribution::concurrent_resolve_calls_complete_with_consistent_attribution covering simultaneous resolve calls sharing the resolver cache

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
Comment thread crates/ironclaw_host_runtime/src/sandbox_process/registry.rs
@github-actions

github-actions Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.56% (308568 / 360641 lines)
  floor:    80.81% (tolerance 0.5pp -> effective floor 80.31%)
  denominator: 360641 lines now vs 377084 at floor capture (-16443 lines, -4.36%) — 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.56% — 308568 / 360641 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.71% 684 / 1565
ironclaw_observability 61.54% 16 / 26
ironclaw_telegram_v2_adapter 62.35% 631 / 1012
ironclaw_authorization 62.98% 609 / 967
ironclaw_memory 70.15% 919 / 1310
ironclaw_trust 73.21% 664 / 907
ironclaw_filesystem 74.31% 4749 / 6391
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_extractors 74.72% 538 / 720
ironclaw_capabilities 75.45% 2879 / 3816
ironclaw_mcp 76.2% 775 / 1017
ironclaw_projects 76.48% 400 / 523
ironclaw_reborn_cli 78.22% 10693 / 13670
ironclaw_telegram_extension 78.59% 962 / 1224
ironclaw_llm 79.29% 21451 / 27054
ironclaw_wasm 79.72% 735 / 922
ironclaw_memory_native 80.97% 3114 / 3846
ironclaw_auth 81.88% 6679 / 8157
ironclaw_first_party_extensions 82.38% 6682 / 8111
ironclaw_events 82.47% 1604 / 1945
ironclaw_host_api 82.51% 9106 / 11036
ironclaw_processes 83.3% 933 / 1120
ironclaw_reborn_identity 83.8% 450 / 537
ironclaw_operator 84.37% 5558 / 6588
ironclaw_secrets 84.56% 2798 / 3309
ironclaw_reborn_config 85.24% 2102 / 2466
ironclaw_skills 85.27% 4493 / 5269
ironclaw_extension_host 85.43% 18822 / 22032
ironclaw_reborn_composition 85.69% 24392 / 28467
ironclaw_run_state 85.77% 458 / 534
ironclaw_triggers 85.92% 2783 / 3239
ironclaw_webui 85.96% 10935 / 12721
ironclaw_network 85.97% 913 / 1062
ironclaw_reborn_event_store 86.51% 1251 / 1446
ironclaw_hooks 86.63% 9931 / 11464
ironclaw_extensions 86.98% 3669 / 4218
ironclaw_common 86.99% 1772 / 2037
ironclaw_approvals 87.18% 1543 / 1770
ironclaw_threads 87.2% 4851 / 5563
ironclaw_product 87.52% 19698 / 22507
ironclaw_reborn_traces 88.13% 11986 / 13600
ironclaw_turns 88.37% 14288 / 16169
ironclaw_slack_extension 88.47% 1934 / 2186
ironclaw_host_runtime 88.83% 19817 / 22310
ironclaw_reborn_openai_compat 89.32% 3780 / 4232
ironclaw_conversations 90.01% 3164 / 3515
ironclaw_resources 90.84% 4474 / 4925
ironclaw_runner 90.87% 17214 / 18944
ironclaw_event_streams 91.24% 1063 / 1165
ironclaw_loop_host 91.86% 16434 / 17891
ironclaw_attachments 93.06% 630 / 677
ironclaw_outbound 93.91% 4101 / 4367
ironclaw_agent_loop 94.65% 9916 / 10477
ironclaw_safety 95.28% 3858 / 4049
ironclaw_first_party_extension_ports 95.62% 3672 / 3840
ironclaw_runtime_policy 96.56% 813 / 842

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

…and concurrent attribution resolve

Addresses review 4784267719 on PR #6695:
- BackgroundJobRegistry::drop_dead now builds a HashSet once instead of
  a per-job linear scan of alive_pids (O(J) not O(J x A)).
- Add background_job_registry_drop_dead_with_empty_alive_pids_removes_all_jobs.
- Add concurrent_resolve_calls_complete_with_consistent_attribution covering
  simultaneous ConnectionAttributionResolver::resolve calls.
- Doc-comment the two known-but-deferred tradeoffs (attribution thundering
  herd, unbounded BackgroundJobRegistry growth) — both need the not-yet-wired
  caller (W6 / exec_transport+reaper) to know the right shape, so building a
  mechanism now would be guessing; left as explicit follow-ups instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 15:06
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6695 July 27, 2026 15: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.

@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/attribution.rs`:
- Around line 682-726: Update
concurrent_resolve_calls_complete_with_consistent_attribution to configure
FakeLookup with a controlled await and Barrier for all 20 lookups, ensuring each
cache miss reaches the synchronization point before any proceeds. Release the
barrier once every spawned resolve is waiting, then retain the existing
attribution assertions and panic/deadlock checks.
🪄 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: 677d7741-67de-4a35-9003-de0cc2284423

📥 Commits

Reviewing files that changed from the base of the PR and between d17e92a and dc28fa5.

📒 Files selected for processing (2)
  • crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
  • crates/ironclaw_host_runtime/src/sandbox_process/registry.rs

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs Outdated
@henrypark133

Copy link
Copy Markdown
Collaborator Author

Addressing both findings from this review round (body-only again — no inline thread was created; ironloop's own note confirms the inline payload was rejected: "Path could not be resolved"):

1. [HIGH, blocking] "Do not cache an attributed identity by reusable IP" (attribution.rs:209) — ALREADY-DECIDED, restating the reasoning since it recurs. This is the known IP-reuse-within-TTL caveat, already documented in-code at resolve()/invalidate(): invalidate() is exactly the seam for synchronous teardown invalidation, but closing the window fully needs a reaper hooked to container lifecycle, which doesn't exist yet because there's no wired caller (W6) to drive it. The doc already says "collapses the staleness window toward zero," not "to zero," specifically because invalidate() isn't race-free against a concurrent in-flight resolve() — that's called out in the doc too. Building synchronous invalidation now, without the real W6 call pattern, risks the wrong shape. Shipping as-is with the caveat documented is the decision; no code change made.

2. [MEDIUM, note] "Keep Docker sandbox parity marked as unwired" (docs/reborn/engine-v2-to-reborn-parity.md:63) — OUT OF SCOPE for this PR. This file is not touched anywhere in this PR's diff (git diff main...HEAD -- docs/reborn/engine-v2-to-reborn-parity.md is empty) — this PR only added/edited docs/reborn/contracts/filesystem.md and docs/reborn/contracts/host-runtime.md. Whether the parity doc's "Covered (note 5)" claim about the Docker sandbox backend is accurate is a pre-existing question unrelated to leaf-scoped mount containment or the sandbox-user-key primitives this PR adds. Not changing it here; flagging as a possible follow-up for whoever owns that doc.

No code changes from this round.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

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

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 1 0 1 7259abf98ff5

Head: 7259abf98ff5326c37af1bd4680a32de02aa4ed8
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Changes requested: the new attribution cache can leak credentials across Docker IP reuse.

Findings

Blocking: 1 / Notes: 0

Blocking findings

1. ❌ [HIGH] Do not cache attributed identities by reusable IP

Location: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:209-210
This cache is keyed only by peer IP, not a TCP connection or container identity. After container A is removed and Docker assigns its IP to container B, B's new connection can arrive within the 5-second TTL and receive A's cached {tenant,user}; the planned credential injector would then use A's credentials for B. Resolve attribution per accepted connection, or make lifecycle invalidation race-free before allowing any reuse.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
Addresses coderabbitai review 4790451629 on PR #6695:
try_exists follows symlinks and reports false for a dangling one, so
a pre-planted dangling symlink at the write target fell through to
the brand-new-file bootstrap path. write_file/append_file open with
O_CREAT, so the OS would create the file wherever the symlink points,
escaping leaf containment. resolve_for_write now checks existence via
symlink_metadata (lstat) and fails closed with SymlinkEscape when a
symlink entry can't be canonicalized, instead of silently falling
through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 20:11
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6695 July 27, 2026 20:11 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

Round 5 triage of two body-only findings (GitHub rejected inline payloads, "Path could not be resolved"):

coderabbitai — dangling final symlink in resolve_for_write (local.rs:145-189, outside diff range). Valid and fixed in f00f1fe. try_exists follows symlinks and reports false for a dangling one, so a pre-planted dangling symlink at the write target fell through to the "brand new file in this leaf" bootstrap path — write_file/append_file open with O_CREAT, so the OS would create the file wherever the symlink points, escaping leaf containment. Added a red/green regression (leaf_scoped_mount_rejects_dangling_final_symlink_escape_on_write, confirmed it failed against the buggy code first) and switched the existence check to symlink_metadata (lstat), failing closed with SymlinkEscape when the entry is a symlink that cannot be canonicalized. Documented in docs/reborn/contracts/filesystem.md (leaf-scoped bullet list + section 15 coverage list).

ironloopai — attribution cache IP-reuse (attribution.rs:209). Already decided (round 4, decision #1): ship the TTL-window cache as-is; the real fix needs the unbuilt reaper, and invalidate()'s doc already says "toward zero" not "to zero" for exactly this reason. Restated on the inline thread (discussion_r3660244126) and resolved rather than re-touched, since this is a repeat of a settled decision.

ironloopai — docs/reborn/engine-v2-to-reborn-parity.md:63 (Docker sandbox parity marking). Out of scope: git diff main...HEAD --stat confirms this PR only touches docs/reborn/contracts/filesystem.md and host-runtime.md; the parity doc was last touched by #6670. Not editing it here — flagging as a follow-up for whoever next touches that doc.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

@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_filesystem/src/local.rs (1)

190-211: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Make leaf containment atomic with the filesystem mutation.

Pathname checks do not hold the checked object. A process permitted to alter leaf-a can replace a checked child directory with a ../leaf-b symlink in the gap: the write flow can create/overwrite sibling data, while create_dir_all can create sibling directories before its later check rejects the path. Use descriptor/capability-rooted no-follow traversal through the final operation, and add a caller-level race regression.

  • crates/ironclaw_filesystem/src/local.rs#L190-L211: retain no-follow directory handles through the eventual file open/create.
  • crates/ironclaw_filesystem/src/local.rs#L227-L234: perform directory creation through the same containment-rooted handles, before reporting success.

As per coding guidelines, ironclaw_filesystem must remain contained against symlink traversal and mount escape, and side-effect gates require caller-level 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_filesystem/src/local.rs` around lines 190 - 211, Replace the
pathname-based final containment and mutation flow around the local filesystem
write/open operation (crates/ironclaw_filesystem/src/local.rs:190-211) with
descriptor/capability-rooted, no-follow traversal that retains directory handles
through the eventual file open/create, preventing symlink or mount escapes
between validation and mutation. Update directory creation in the corresponding
create-directory path (crates/ironclaw_filesystem/src/local.rs:227-234) to use
the same containment-rooted handles before reporting success, and add a
caller-level race regression test covering replacement of a checked child with a
symlink.

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_filesystem/src/local.rs`:
- Around line 190-211: Replace the pathname-based final containment and mutation
flow around the local filesystem write/open operation
(crates/ironclaw_filesystem/src/local.rs:190-211) with
descriptor/capability-rooted, no-follow traversal that retains directory handles
through the eventual file open/create, preventing symlink or mount escapes
between validation and mutation. Update directory creation in the corresponding
create-directory path (crates/ironclaw_filesystem/src/local.rs:227-234) to use
the same containment-rooted handles before reporting success, and add a
caller-level race regression test covering replacement of a checked child with a
symlink.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 97fe62f9-07eb-43b2-84ae-823b26f49c04

📥 Commits

Reviewing files that changed from the base of the PR and between 7259abf and f00f1fe.

📒 Files selected for processing (2)
  • crates/ironclaw_filesystem/src/local.rs
  • docs/reborn/contracts/filesystem.md

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

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 2 0 2 f00f1fe67966

Head: f00f1fe679669397a6a6aed83c61098c68bdcf2e
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

The comparison reintroduces the legacy Docker process-sandbox backend that base main removed. Its mount and egress behavior do not satisfy the current sandbox isolation contract.

Findings

Blocking: 2 / Notes: 0

Blocking findings

1. ❌ [HIGH] Derive Docker mounts from the request scope

Location: crates/ironclaw_process_sandbox/src/docker.rs:486
DockerProcessSandboxConfig supplies one fixed workspace_host_path, and this invocation builder never receives SandboxProcessRequest.scope or a trusted MountView. If this re-exported executor is selected, every tenant is therefore given the same workspace bind and can read or modify another scope's files. Keep this removed legacy backend out of the PR, or derive each source from the request's trusted mount grants before creating the container.

2. ❌ [HIGH] Enforce broker-only egress at the network layer

Location: crates/ironclaw_process_sandbox/src/docker.rs:426
Credentialed runs select Docker's default bridge network. The proxy environment variables and IRONCLAW_EGRESS_LOCKDOWN string are opt-in conventions, so sandboxed code can unset/bypass them and connect directly to arbitrary hosts, bypassing runtime_hosts and the claimed direct-egress lockdown. Use an isolated network/socket design that can reach only the broker, and add a real enforcement test.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.
Inline review fallback

Inline comment projection fell back to a body-only PR Review because GitHub rejected the inline payload.
Reason: Unprocessable Entity: "Path could not be resolved and Path could not be resolved" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request

IronLoop preserved the inline review comment payloads below instead of dropping them.

Inline fallback 1: crates/ironclaw_process_sandbox/src/docker.rs:486

This uses one fixed configured workspace path without request scope or MountView resolution. Selecting this executor would bind the same workspace into every tenant's command. Derive sources from trusted per-request mount grants before container creation.

Inline fallback 2: crates/ironclaw_process_sandbox/src/docker.rs:426

bridge permits direct outbound traffic; proxy environment variables do not enforce broker-only egress. A command can bypass them and ignore runtime_hosts, so use a network/socket design that only permits the broker.

coderabbitai (round 7) flagged that leaf containment checks aren't
atomic with the mutation they gate. The gap is real but matches the
already-deferred residual documented on resolve_for_write's re-rooting
step (full fix needs openat/O_NOFOLLOW/cap-std, tracked via PR #2996
review) — extend that documentation to resolve_for_create_dir_all and
delete instead of re-litigating the same heavy-lift follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 21:34
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6695 July 27, 2026 21:34 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

Reply to coderabbitai's review (round 7, review id 4791130037) — body-only "outside diff range" finding on local.rs:190-211/227-234.

Valid, not new. The TOCTOU window between the containment check and the eventual mutation is real: it's the same residual already called out in the resolve_for_write comment (local.rs:200-206, "Robust defense (openat / O_NOFOLLOW / cap-std) is tracked as a follow-up; see PR #2996 review"). Closing it fully means replacing this file's whole canonicalize-then-check model with descriptor/capability-rooted traversal — a heavy-lift architectural rewrite, not a fix that belongs in a leaf-containment hardening PR under review pressure.

Resolution (b8650a4): extended that same documentation to resolve_for_create_dir_all and delete, which have the identical residual and weren't covered by the original comment. No behavior change — this is the established defer-and-document pattern already used elsewhere in this PR (e.g. the attribution-cache IP-reuse-within-TTL decision). Full fd-rooted traversal remains tracked as a follow-up, not blocking this PR.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

Reply to ironloopai's review (round 7, review id 4791164423) — both blocking findings are on crates/ironclaw_process_sandbox/src/docker.rs:426 and :486.

Invalid for this PR — file not touched. git diff main...HEAD --stat confirms this PR's diff touches only crates/ironclaw_filesystem/src/local.rs, crates/ironclaw_host_runtime/src/**, crates/ironclaw_safety/**, crates/ironclaw_secrets/**, and two docs/reborn/contracts/*.md files — ironclaw_process_sandbox/src/docker.rs isn't in the diff and its last change (1e50ddfee) predates this branch. This review appears to be evaluating that file against a different comparison base than this PR's actual head/base, not this PR's changes. No action taken; nothing to fix here.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

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

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 1 0 1 b8650a4d690e

Head: b8650a4d690ed3e17a762969ae74f26cd6339a6c
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Found a high-severity cross-tenant credential-attribution flaw: positive attribution is cached solely by reusable Docker IP.

Findings

Blocking: 1 / Notes: 0

Blocking findings

1. ❌ [HIGH] Do not cache attributed identities by reusable IP

Location: crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs:209
A positive cache entry is keyed only by peer_ip. If Docker tears down tenant A's container and assigns that IP to tenant B within the 5-second TTL, B's new connection receives A's cached identity. The future credential-injection consumer would then inject A's credentials into B's request. invalidate does not close this path: no lifecycle caller is wired, and the documented query/invalidate race can reinsert stale data. Do not cache positive attributions by bare IP; revalidate each new connection or bind cache validity to a non-reusable container/lifecycle generation.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

Comment thread crates/ironclaw_host_runtime/src/sandbox_process/attribution.rs
…ed-containment

# Conflicts:
#	crates/ironclaw_host_runtime/src/sandbox_process.rs
Copilot AI review requested due to automatic review settings July 27, 2026 22:13
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6695 July 27, 2026 22:13 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 bc0726d into main Jul 28, 2026
65 checks passed
@henrypark133
henrypark133 deleted the sandbox/pr3-leaf-scoped-containment branch July 28, 2026 04:08
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…ity primitives (nearai#6695)

* feat(sandbox): leaf-scoped mount containment + per-user sandbox identity primitives

Ships two related, unwired-by-design slices of the persistent per-user
sandbox program:

- ironclaw_filesystem: leaf-scoped mount containment. resolve_joined now
  returns a per-request containment_root that, for a leaf_scoped mount, is
  host_root/<first-tail-segment> instead of the shared host_root — closing
  a same-mount cross-leaf symlink escape a plain mount_local containment
  check would miss. mount_local_per_leaf is the constructor; a bare-root
  request against such a mount is rejected outright (no safe containment
  root for "every caller's leaf").

- ironclaw_host_runtime: identity + attribution primitives for the
  persistent per-user sandbox container model — RebornSandboxUserKey
  ({tenant,user}-only container/workspace key), the labels-as-identity
  registry (Docker label helpers, SandboxActivityRegistry,
  BackgroundJobRegistry), and ConnectionAttributionResolver (source-IP to
  {tenant,user} resolution for the shared egress proxy, design decision D9).

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

* fix(sandbox): attribution real-docker test reuses connect_docker() fallback

docker_gate::docker_available() shells out to the docker CLI, which resolves the daemon through whatever context is active (Colima, Docker Desktop, a remote host). The test then connected directly via Docker::connect_with_local_defaults(), which only honors DOCKER_HOST or the hardcoded /var/run/docker.sock, so the gate could pass while the connection still failed on any machine using a non-default socket.

Reuse sandbox_process::connect_docker() instead of reimplementing resolution: it already tries connect_with_local_defaults() then falls back through unix_socket_candidates(), the same path production containers connect through. A connect_docker() failure now prints a SKIP line rather than panicking.

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

* fix(sandbox): address PR review — leaf-creation bug, attribution hygiene, docker CI gate

Leaf-scoped mounts rejected a brand-new leaf's first write/create_dir_all
(ensure_existing_ancestor_contained had no bootstrap case for the shared
host_root when a caller's leaf doesn't exist yet); accept that one ancestor
now and add regression coverage for write-path creation, write-path
cross-leaf symlink escape, and create_dir_all bootstrap.

Attribution cache: sweep expired entries on miss so a long-running resolver
doesn't grow the cache unboundedly, and log the missing/malformed-label
fail-closed branch like its sibling branches. Add coverage for malformed/
missing container network IPs.

Docker CI gate: the attribution real-Docker test's connect_docker() failure
branch always skipped, even under IRONCLAW_REQUIRE_DOCKER_TESTS=1 — panic
in that mode instead, matching docker_gate's existing fail-closed pattern.
Pull busybox:1.36 before create_container so the test doesn't depend on a
pre-warmed local image cache (this is what broke it in CI).

registry.rs/attribution.rs: crate::-rooted imports per repo convention;
trim sandbox_process.rs's module header to stable ownership, not PR-state.
Add BackgroundJobRegistry, malformed-candidate-parsing, and concurrent
SandboxActivityRegistry coverage.

docs/reborn/contracts/host-runtime.md: one forward-pointing sentence
noting RebornSandboxUserKey's future coarser identity model doesn't yet
supersede the scope-derived identity this contract already documents.

architecture ratchet: baseline the two new test/dead-code seams this
introduces (attribution.rs dead-code method x4, test-support method x1).

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

* fix(sandbox): O(1) drop_dead pruning + coverage for empty-alive-pids and concurrent attribution resolve

Addresses review 4784267719 on PR nearai#6695:
- BackgroundJobRegistry::drop_dead now builds a HashSet once instead of
  a per-job linear scan of alive_pids (O(J) not O(J x A)).
- Add background_job_registry_drop_dead_with_empty_alive_pids_removes_all_jobs.
- Add concurrent_resolve_calls_complete_with_consistent_attribution covering
  simultaneous ConnectionAttributionResolver::resolve calls.
- Doc-comment the two known-but-deferred tradeoffs (attribution thundering
  herd, unbounded BackgroundJobRegistry growth) — both need the not-yet-wired
  caller (W6 / exec_transport+reaper) to know the right shape, so building a
  mechanism now would be guessing; left as explicit follow-ups instead.

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

* fix(sandbox): force real cache-miss overlap in attribution concurrency test

FakeLookup::containers_on_network returned immediately with no yield
point, so the 20 spawned resolve() tasks could run to sequential
completion without ever actually overlapping in the miss/query/insert
window the test claims to exercise. Add an optional Barrier that all
callers wait on inside containers_on_network, forcing genuine
concurrent cache misses before any insert proceeds.

Addresses CodeRabbit review 4788508880.

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

* fix(sandbox): address new PR nearai#6695 review round (CI-hang test, silent-ok docs, IPv6 attribution, typed mount resolution, key codec dedup

- attribution.rs: bound the concurrent-resolve barrier test with a timeout
  so a regression can't hang CI; add silent-ok rationale comments on the
  Docker-boundary .ok() parses; fix container_addresses_on_network to also
  read bollard's global_ipv6_address field (an IPv6-only peer was
  previously never matchable); add empty-listing and IPv6 coverage.
- registry.rs: module header now names all three responsibilities
  (label codec, activity registry, background-job registry); add a
  concurrent record/jobs_for/drop_dead test for BackgroundJobRegistry to
  match the existing SandboxActivityRegistry coverage.
- user_key.rs: clarify that RebornSandboxScopeKey remains authoritative
  for the currently-wired transport; RebornSandboxUserKey is reserved for
  the future persistent per-user transport.
- docker_gate.rs: header now describes the daemon-only gate accurately
  instead of overclaiming a required ironclaw-worker image.
- local.rs: resolve_joined now returns a typed ResolvedMountPath (joined,
  containment_root, bootstrap_root) instead of a positional tuple plus a
  separately re-derived bootstrap_root at two of three call sites.
- New key_codec module: shared length-prefixed encoding + SHA-256 digest
  for RebornSandboxScopeKey and RebornSandboxUserKey, replacing two
  independently-maintained copies of the same framing.

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

* refactor(sandbox): split attribution.rs test module into its own file

My prior fixes pushed attribution.rs from 951 to 1029 lines, crossing the
hard 1000-line threshold. Moved #[cfg(test)] mod tests (FakeLookup harness,
17 unit tests, the gated real-Docker integration test) into a sibling
attribution_tests.rs via #[path], matching the file's existing docker_gate
#[path] pattern. Pure move, no behavior change: production attribution.rs
is now 367 lines.

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

* fix(sandbox): address round-6 review findings on nearai#6695

- crate::-qualify key_codec imports in user_key.rs/scope_key.rs (matches
  the PR's own crate:: convention already used for registry/attribution)
- add missing silent-ok rationale for the dropped created_at parse error
  in UserContainerCandidate::from_summary
- gate the two cross-leaf symlink-escape tests in local.rs with
  #[cfg(unix)] (std::os::unix::fs::symlink does not compile on the
  windows release target)
- document mount_local_per_leaf's bare-root-denial, first-use-bootstrap,
  and per-leaf symlink-containment contract in filesystem.md
- add missing-tenant and malformed-tenant-label fail-closed tests to
  attribution_tests.rs (existing coverage only exercised the user field)
- correct the ConnectionAttributionResolver doc comments: invalidate()
  collapses staleness "toward" zero, not "to" zero — a concurrent
  in-flight resolve() can still re-insert a stale entry after
  invalidate() removes it. Not fixed (no caller exists yet to fix a
  race against), but the doc must not overclaim a guarantee it does
  not have.

* fix(filesystem): reject dangling final symlink in resolve_for_write

Addresses coderabbitai review 4790451629 on PR nearai#6695:
try_exists follows symlinks and reports false for a dangling one, so
a pre-planted dangling symlink at the write target fell through to
the brand-new-file bootstrap path. write_file/append_file open with
O_CREAT, so the OS would create the file wherever the symlink points,
escaping leaf containment. resolve_for_write now checks existence via
symlink_metadata (lstat) and fails closed with SymlinkEscape when a
symlink entry can't be canonicalized, instead of silently falling
through.

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

* docs(filesystem): document TOCTOU residual on create_dir_all/delete

coderabbitai (round 7) flagged that leaf containment checks aren't
atomic with the mutation they gate. The gap is real but matches the
already-deferred residual documented on resolve_for_write's re-rooting
step (full fix needs openat/O_NOFOLLOW/cap-std, tracked via PR nearai#2996
review) — extend that documentation to resolve_for_create_dir_all and
delete instead of re-litigating the same heavy-lift follow-up.

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

---------

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

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6695 — cd10cd5b Deployed Jul 27, 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: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants