Expose sandboxed read_file/write_file helpers to inline code (Python_exec), gated by safe_mode #93

Closed
opened 2026-07-31 19:26:18 +00:00 by CoreRasurae · 0 comments
Member

Metadata

  • Commit Message: feat(agents): expose sandboxed file helpers to inline code
  • Branch: feature/m1-inline-code-sandboxed-file-access

Background and context

Inline-code tools (§4.5.2) — e.g. a Python_exec-style type: tool agent with
a code body — run in the restricted sandbox (§13.2). Today that sandbox
exposes no filesystem access at all: open() is not in the §13.2.1 built-in set
and §13.2.3 prohibits arbitrary filesystem I/O unconditionally. That is
conformant, but it means inline code cannot read or write file contents
dynamically from within a single code body; the only sanctioned path is static
composition (file_read tool → inline tool → file_write tool), which does not
cover dynamic or multi-file access.

ADR-2035 (docs/adr/ADR-2035-inline-code-sandboxed-file-access.md) decides
to close this gap with sanctioned helper functions rather than raw open():
two callables, read_file/write_file, always injected into the inline
sandbox, reusing the existing file_read/file_write validated cores. Per the
ADR's D-7 correction, the two callables are gated solely by the tool agent's
own safe_mode config field — the same flag that already governs the
file_read/file_write tools — rather than by the per-invocation
_unsafe_mode context flag. This issue implements that decision and applies
the specification amendments the ADR mandates.

Gating: implementation of this issue is contingent on ADR-2035 being
accepted
. It should not begin until the ADR is merged.

Current behavior

  • Inline code cannot call open() in any mode; no read_file/write_file
    facility exists.
  • File contents can only reach inline code by being piped in as input_data
    from a separately-wired file_read tool.

Expected behavior

Per ADR-2035 (as corrected by D-7), the inline sandbox always gains exactly
two injected local callables (like json), and nothing else widens. Their
behavior is gated solely by the tool agent's own safe_mode config field —
mirroring how safe_mode already gates the file_read/file_write tools:

  • read_file(path, max_chars=None, offset=0) -> str — returns the raw decoded
    file contents, reusing the file_read core (incl. ADR-2033 offset/max_chars
    windowing). Returns content, not the [FILE_READ_SUCCESS] LLM envelope.
    Confined to the sandbox root when safe_mode is true; unrestricted when
    false.
  • write_file(path, content, mode="w") -> int — writes via the file_write
    core (§4.5.5 modes), returns characters written. Refused unconditionally
    when safe_mode is true; unrestricted when false.

Neither callable is ever absent — referencing read_file/write_file never
raises NameError.

Acceptance criteria

  • read_file and write_file are always available as injected locals
    inside every inline-code body, regardless of safe_mode; referencing
    either name never raises NameError.
  • open, os, io, pathlib, and file descriptors remain unavailable to
    inline code (the §13.2.1 built-in table is unchanged).
  • read_file returns the raw decoded content as str, honoring max_chars
    and offset with the same semantics as the file_read tool.
  • write_file writes via the file_write core, honors w/a/insert
    modes, and returns the number of characters written.
  • Containment (safe_mode: true only): read_file rejects .. traversal,
    ~ expansion, and any path whose resolved real path (symlinks followed)
    falls outside the sandbox root; a violation raises ValueError naming
    the offending path.
  • Mode gating: with safe_mode: true, write_file is refused
    unconditionally, raising ValueError before any I/O is attempted,
    regardless of the invocation context. With safe_mode: false, both
    read_file and write_file are unrestricted — no sandbox-root check
    applies to either.
  • docs/index.md is updated per ADR-2035 D-6 (§13.2.3 reworded, §4.5.2 lists
    the helpers, §13.2.1 note, §13.3 cross-reference) in the same change.
  • The §13.2.2 expression sandbox (transform.fn, bridge predicates) is
    unchanged and gains no file access.

Supporting information

  • Decision record: docs/adr/ADR-2035-inline-code-sandboxed-file-access.md
    (Status: Approved; amended by D-7 to correct the mode-gating model).
  • Spec: docs/index.md — §4.5.2 (inline tools / injected facilities), §4.5.1 &
    §4.5.5 (file_read/file_write tools and modes), §13.2.1 (inline built-ins),
    §13.2.3 (prohibited capabilities), §13.3 (filesystem boundaries), §4.5.4
    (safe-mode restrictions).
  • Reuses the ADR-2033 offset/max_chars windowing for read_file.

Subtasks

  • Extract the file_read/file_write validation+I/O cores so they can back
    both the built-in tools and the injected helpers (single validation
    surface).
  • Implement the sandbox-root containment check (realpath resolution,
    symlink-escape rejection, ../~ rejection), raising ValueError.
  • Implement read_file and write_file over those cores; gate both
    solely on the agent's own safe_mode (read_file confined,
    write_file refused, when true; both unrestricted when false).
  • Inject the helpers into the inline-code namespace unconditionally; they
    are never absent, in either mode.
  • Update docs/index.md per ADR-2035 D-6/D-7, in the same commit as the
    code.
  • Tests (Behave): helper availability (always bound), read confined to the
    sandbox root under safe_mode: true, write refused under safe_mode: true, read/write unrestricted under safe_mode: false, offset/max_chars,
    write modes, and sandbox-escape attempts (.., absolute, ~,
    symlink-out-of-root).
  • Tests (Robot): integration exercising inline code reading and writing a
    real file within the sandbox (safe_mode: false), and rejecting an
    escape attempt (safe_mode: true).
  • Update CHANGELOG with a user-facing entry.
  • Verify coverage >= 97% via nox -s coverage_report.
  • Run nox (all default sessions), fix any errors.

Definition of Done

This issue is complete when:

  • ADR-2035 is accepted and its §13 amendments are reflected in docs/index.md
    in this change.
  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line matches the Commit Message in
    Metadata exactly.
  • The commit is pushed to the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a PR to master, reviewed, and merged.
## Metadata - **Commit Message:** `feat(agents): expose sandboxed file helpers to inline code` - **Branch:** `feature/m1-inline-code-sandboxed-file-access` ## Background and context Inline-code tools (§4.5.2) — e.g. a `Python_exec`-style `type: tool` agent with a `code` body — run in the restricted sandbox (§13.2). Today that sandbox exposes no filesystem access at all: `open()` is not in the §13.2.1 built-in set and §13.2.3 prohibits arbitrary filesystem I/O unconditionally. That is conformant, but it means inline code cannot read or write file contents dynamically from within a single code body; the only sanctioned path is static composition (`file_read` tool → inline tool → `file_write` tool), which does not cover dynamic or multi-file access. **ADR-2035** (`docs/adr/ADR-2035-inline-code-sandboxed-file-access.md`) decides to close this gap with **sanctioned helper functions** rather than raw `open()`: two callables, `read_file`/`write_file`, always injected into the inline sandbox, reusing the existing `file_read`/`file_write` validated cores. Per the ADR's D-7 correction, the two callables are gated solely by the tool agent's own `safe_mode` config field — the same flag that already governs the `file_read`/`file_write` tools — rather than by the per-invocation `_unsafe_mode` context flag. This issue implements that decision and applies the specification amendments the ADR mandates. > Gating: implementation of this issue is contingent on **ADR-2035 being > accepted**. It should not begin until the ADR is merged. ## Current behavior - Inline code cannot call `open()` in any mode; no `read_file`/`write_file` facility exists. - File contents can only reach inline code by being piped in as `input_data` from a separately-wired `file_read` tool. ## Expected behavior Per ADR-2035 (as corrected by D-7), the inline sandbox always gains exactly two injected local callables (like `json`), and nothing else widens. Their behavior is gated solely by the tool agent's own `safe_mode` config field — mirroring how `safe_mode` already gates the `file_read`/`file_write` tools: - `read_file(path, max_chars=None, offset=0) -> str` — returns the raw decoded file contents, reusing the `file_read` core (incl. ADR-2033 offset/max_chars windowing). Returns content, not the `[FILE_READ_SUCCESS]` LLM envelope. Confined to the sandbox root when `safe_mode` is `true`; unrestricted when `false`. - `write_file(path, content, mode="w") -> int` — writes via the `file_write` core (§4.5.5 modes), returns characters written. Refused unconditionally when `safe_mode` is `true`; unrestricted when `false`. Neither callable is ever absent — referencing `read_file`/`write_file` never raises `NameError`. ## Acceptance criteria - [ ] `read_file` and `write_file` are always available as injected locals inside every inline-code body, regardless of `safe_mode`; referencing either name never raises `NameError`. - [ ] `open`, `os`, `io`, `pathlib`, and file descriptors remain unavailable to inline code (the §13.2.1 built-in table is unchanged). - [ ] `read_file` returns the raw decoded content as `str`, honoring `max_chars` and `offset` with the same semantics as the `file_read` tool. - [ ] `write_file` writes via the `file_write` core, honors `w`/`a`/`insert` modes, and returns the number of characters written. - [ ] Containment (`safe_mode: true` only): `read_file` rejects `..` traversal, `~` expansion, and any path whose resolved real path (symlinks followed) falls outside the sandbox root; a violation raises `ValueError` naming the offending path. - [ ] Mode gating: with `safe_mode: true`, `write_file` is refused unconditionally, raising `ValueError` before any I/O is attempted, regardless of the invocation context. With `safe_mode: false`, both `read_file` and `write_file` are unrestricted — no sandbox-root check applies to either. - [ ] `docs/index.md` is updated per ADR-2035 D-6 (§13.2.3 reworded, §4.5.2 lists the helpers, §13.2.1 note, §13.3 cross-reference) in the same change. - [ ] The §13.2.2 expression sandbox (`transform.fn`, bridge predicates) is unchanged and gains no file access. ## Supporting information - Decision record: `docs/adr/ADR-2035-inline-code-sandboxed-file-access.md` (Status: Approved; amended by D-7 to correct the mode-gating model). - Spec: `docs/index.md` — §4.5.2 (inline tools / injected facilities), §4.5.1 & §4.5.5 (`file_read`/`file_write` tools and modes), §13.2.1 (inline built-ins), §13.2.3 (prohibited capabilities), §13.3 (filesystem boundaries), §4.5.4 (safe-mode restrictions). - Reuses the ADR-2033 `offset`/`max_chars` windowing for `read_file`. ## Subtasks - [ ] Extract the `file_read`/`file_write` validation+I/O cores so they can back both the built-in tools and the injected helpers (single validation surface). - [ ] Implement the sandbox-root containment check (realpath resolution, symlink-escape rejection, `..`/`~` rejection), raising `ValueError`. - [ ] Implement `read_file` and `write_file` over those cores; gate both solely on the agent's own `safe_mode` (`read_file` confined, `write_file` refused, when `true`; both unrestricted when `false`). - [ ] Inject the helpers into the inline-code namespace unconditionally; they are never absent, in either mode. - [ ] Update `docs/index.md` per ADR-2035 D-6/D-7, in the same commit as the code. - [ ] Tests (Behave): helper availability (always bound), read confined to the sandbox root under `safe_mode: true`, write refused under `safe_mode: true`, read/write unrestricted under `safe_mode: false`, offset/max_chars, write modes, and sandbox-escape attempts (`..`, absolute, `~`, symlink-out-of-root). - [ ] Tests (Robot): integration exercising inline code reading and writing a real file within the sandbox (`safe_mode: false`), and rejecting an escape attempt (`safe_mode: true`). - [ ] Update CHANGELOG with a user-facing entry. - [ ] Verify coverage >= 97% via `nox -s coverage_report`. - [ ] Run `nox` (all default sessions), fix any errors. ## Definition of Done This issue is complete when: - ADR-2035 is accepted and its §13 amendments are reflected in `docs/index.md` in this change. - All subtasks above are completed and checked off. - A Git commit is created where the first line matches the Commit Message in Metadata exactly. - The commit is pushed to the branch matching the Branch in Metadata exactly. - The commit is submitted as a PR to `master`, reviewed, and merged.
CoreRasurae added this to the v2.1.0 milestone 2026-07-31 19:26:18 +00:00
CoreRasurae changed title from Expose sandboxed read_file/write_file helpers to inline code (Python_exec) in unsafe mode to Expose sandboxed read_file/write_file helpers to inline code (Python_exec), gated by safe_mode 2026-08-06 17:14:05 +00:00
CoreRasurae 2026-08-07 16:43:42 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveractors-core#93
No description provided.