shellsim 0.1.0__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {shellsim-0.1.0 → shellsim-0.1.3}/Cargo.lock +1 -1
- {shellsim-0.1.0 → shellsim-0.1.3}/Cargo.toml +4 -3
- shellsim-0.1.3/PKG-INFO +119 -0
- shellsim-0.1.3/README.md +95 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/pyproject.toml +27 -2
- {shellsim-0.1.0 → shellsim-0.1.3}/python/native/Cargo.lock +2 -2
- {shellsim-0.1.0 → shellsim-0.1.3}/python/native/Cargo.toml +5 -1
- shellsim-0.1.3/python/shellsim/__main__.py +5 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/python/shellsim/_api.py +7 -17
- shellsim-0.1.3/python/shellsim/_cli.py +175 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/builtins.rs +305 -64
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/fs.rs +392 -72
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/hashing.rs +76 -9
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/mod.rs +77 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/printf.rs +45 -9
- shellsim-0.1.3/src/commands/sort.rs +146 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/system.rs +119 -25
- shellsim-0.1.3/src/commands/text.rs +1976 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/util.rs +13 -1
- {shellsim-0.1.0 → shellsim-0.1.3}/src/descriptors.rs +121 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/exec.rs +97 -25
- {shellsim-0.1.0 → shellsim-0.1.3}/src/expand.rs +64 -7
- {shellsim-0.1.0 → shellsim-0.1.3}/src/interp.rs +191 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/pseudo_fs.rs +20 -2
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/heap.rs +18 -1
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/mod.rs +23 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/native.rs +138 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/number.rs +1 -1
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/object_model.rs +154 -4
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/parser.rs +38 -1
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/protocol.rs +12 -2
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/mod.rs +8 -1
- shellsim-0.1.3/src/python/stdlib/numpy.rs +3951 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/vm.rs +382 -18
- {shellsim-0.1.0 → shellsim-0.1.3}/src/resources.rs +5 -0
- shellsim-0.1.0/PKG-INFO +0 -317
- shellsim-0.1.0/README.md +0 -293
- shellsim-0.1.0/src/commands/sort.rs +0 -55
- shellsim-0.1.0/src/commands/text.rs +0 -1148
- {shellsim-0.1.0 → shellsim-0.1.3}/LICENSE +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/python/native/src/lib.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/python/shellsim/__init__.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/python/shellsim/py.typed +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/rust-toolchain.toml +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/bin/shellsim-python.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/clock.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/archives.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/awk.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/echo.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/git.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/makecmd.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/net.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/patch.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/pkg.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/proc.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/rgcmd.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/tarcmd.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/zipcmd.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/harness.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/harness_manager.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/hashes.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/host_ingest.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/jqcmd.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/lib.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/main.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/mcp.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/net.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/netcmd.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/process.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/ast.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/bytecode.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/compiler.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/filesystem.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/lexer.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/process.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/source.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/argparse.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/base64.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/bisect.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/collections.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/core.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/dataclasses.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/enum.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/frozen.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/functools.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/hashlib.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/heapq.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/itertools.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/json.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/math.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/os.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/pytest.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/re.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/abc.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/base64.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/codecs.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/collections.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/csv.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/datetime.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/glob.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/hashlib.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/io.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/json.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/logging.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/os.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/pathlib.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/struct.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/subprocess.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/tempfile.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/uuid.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/zlib.py +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/string.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/struct.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/subprocess.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/sys.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/time.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/typing.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/unittest.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/vfs.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/zlib.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/python/token.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/sandbox.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/scenario.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/scheduler.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/shell.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/telemetry.rs +0 -0
- {shellsim-0.1.0 → shellsim-0.1.3}/src/vfs.rs +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "shellsim"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
edition = "2021"
|
|
5
|
-
description = "
|
|
5
|
+
description = "In-process BusyBox-like environment for RL rollouts and agent tests"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
repository = "https://github.com/rjpower/shellsim"
|
|
@@ -58,4 +58,5 @@ python = []
|
|
|
58
58
|
opt-level = 1
|
|
59
59
|
|
|
60
60
|
[profile.release]
|
|
61
|
-
opt-level =
|
|
61
|
+
opt-level = "s"
|
|
62
|
+
lto = false
|
shellsim-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shellsim
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
6
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
7
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
8
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
+
Classifier: Programming Language :: Rust
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Summary: In-process BusyBox-like environment for RL rollouts and agent tests
|
|
16
|
+
Keywords: agents,containers,reinforcement-learning,sandbox,simulation,testing
|
|
17
|
+
Author: The shellsim authors
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
20
|
+
Project-URL: Homepage, https://github.com/rjpower/shellsim
|
|
21
|
+
Project-URL: Issues, https://github.com/rjpower/shellsim/issues
|
|
22
|
+
Project-URL: Repository, https://github.com/rjpower/shellsim
|
|
23
|
+
|
|
24
|
+
# shellsim
|
|
25
|
+
|
|
26
|
+
Shellsim is a BusyBox for containers: one small, deterministic process that provides a useful
|
|
27
|
+
Unix-shaped environment without starting a VM, container runtime, or host subprocess. It is built
|
|
28
|
+
for experimentation and testing with reinforcement-learning rollouts and agentic environments,
|
|
29
|
+
where fast startup, reproducibility, isolation, and explicit resource limits matter more than
|
|
30
|
+
cycle-accurate emulation.
|
|
31
|
+
|
|
32
|
+
Shell programs, common command-line tools, logical processes, and Python run in-process against an
|
|
33
|
+
in-memory filesystem. Simulated code cannot access the host filesystem, processes, network,
|
|
34
|
+
environment, or clock. A trusted harness may copy a selected project into the virtual filesystem
|
|
35
|
+
before execution; changes never write back to the host.
|
|
36
|
+
|
|
37
|
+
## Compatibility
|
|
38
|
+
|
|
39
|
+
Shellsim aims for broad compatibility inside clear boundaries. A supported facility should handle
|
|
40
|
+
almost all ordinary uses, even when obscure flags or legacy behavior remain out of scope. A module
|
|
41
|
+
or command with no coherent useful subset is omitted instead of being exposed as a misleading
|
|
42
|
+
stub. Unsupported syntax, options, executable formats, and capabilities fail visibly and are
|
|
43
|
+
included in structured results.
|
|
44
|
+
|
|
45
|
+
The current environment includes:
|
|
46
|
+
|
|
47
|
+
- a Bash-like shell with pipelines, redirections, functions, common expansions, control flow,
|
|
48
|
+
background jobs, signals, and job control;
|
|
49
|
+
- common filesystem, text, archive, Git, Make, process, and system commands;
|
|
50
|
+
- deterministic virtual time, network fixtures, `/proc`, `/dev`, processes, descriptors, and
|
|
51
|
+
bounded pipes;
|
|
52
|
+
- a mostly complete Python language runtime with a deliberately selected standard-library and
|
|
53
|
+
third-party module surface.
|
|
54
|
+
|
|
55
|
+
Python is source-compatible where supported, not ABI-compatible with CPython. Native extensions,
|
|
56
|
+
package installation, compilers, and arbitrary machine code are outside the simulation boundary.
|
|
57
|
+
See [Python in shellsim](docs/python.md) for the current contract.
|
|
58
|
+
|
|
59
|
+
## Install and run
|
|
60
|
+
|
|
61
|
+
Install the Python package and console command:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
python -m pip install shellsim
|
|
65
|
+
shellsim -c 'printf "b\na\n" | sort'
|
|
66
|
+
shellsim --root ./project -c 'python3.14 test.py'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`--root` copies the selected host tree into a disposable `/work` snapshot. With no `-c` and a
|
|
70
|
+
terminal attached, `shellsim` starts a persistent interactive session.
|
|
71
|
+
|
|
72
|
+
To build the Rust binaries from source:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
cargo build --release
|
|
76
|
+
./target/release/shellsim -c 'echo hello'
|
|
77
|
+
./target/release/shellsim eval --cpu 100k --memory 8m -c 'make test'
|
|
78
|
+
./target/release/shellsim-python ./project/main.py -- arg1
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
|
|
82
|
+
exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
|
|
83
|
+
|
|
84
|
+
The Python API exposes fresh and persistent environments:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import shellsim
|
|
88
|
+
|
|
89
|
+
environment = shellsim.Environment(cpu=100_000)
|
|
90
|
+
environment.write_file("/work/main.py", "print(6 * 7)\n")
|
|
91
|
+
result = environment.run("python3.14 /work/main.py")
|
|
92
|
+
assert result.returncode == 0
|
|
93
|
+
assert result.stdout == b"42\n"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Agent harness
|
|
97
|
+
|
|
98
|
+
`shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
|
|
99
|
+
bounded execution, streaming actions, VFS operations, checkpoints, workspace diffs, process and
|
|
100
|
+
resource inspection, and deterministic session forks. `shellsim mcp` exposes the same environment
|
|
101
|
+
as a stdio MCP server. `shellsim replay scenario.ndjson` reruns checked action transcripts.
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
printf '%s\n' \
|
|
105
|
+
'{"id":1,"op":"execute","source":"printf hello > result"}' \
|
|
106
|
+
'{"id":2,"op":"workspace_diff"}' \
|
|
107
|
+
| shellsim serve --root ./project
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Resource model
|
|
111
|
+
|
|
112
|
+
CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
|
|
113
|
+
usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
|
|
114
|
+
64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
|
|
115
|
+
Exhaustion is observable and never falls back to an ambient host implementation.
|
|
116
|
+
|
|
117
|
+
For internals and contribution workflow, see [implementation](docs/implementation.md),
|
|
118
|
+
[Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
119
|
+
|
shellsim-0.1.3/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# shellsim
|
|
2
|
+
|
|
3
|
+
Shellsim is a BusyBox for containers: one small, deterministic process that provides a useful
|
|
4
|
+
Unix-shaped environment without starting a VM, container runtime, or host subprocess. It is built
|
|
5
|
+
for experimentation and testing with reinforcement-learning rollouts and agentic environments,
|
|
6
|
+
where fast startup, reproducibility, isolation, and explicit resource limits matter more than
|
|
7
|
+
cycle-accurate emulation.
|
|
8
|
+
|
|
9
|
+
Shell programs, common command-line tools, logical processes, and Python run in-process against an
|
|
10
|
+
in-memory filesystem. Simulated code cannot access the host filesystem, processes, network,
|
|
11
|
+
environment, or clock. A trusted harness may copy a selected project into the virtual filesystem
|
|
12
|
+
before execution; changes never write back to the host.
|
|
13
|
+
|
|
14
|
+
## Compatibility
|
|
15
|
+
|
|
16
|
+
Shellsim aims for broad compatibility inside clear boundaries. A supported facility should handle
|
|
17
|
+
almost all ordinary uses, even when obscure flags or legacy behavior remain out of scope. A module
|
|
18
|
+
or command with no coherent useful subset is omitted instead of being exposed as a misleading
|
|
19
|
+
stub. Unsupported syntax, options, executable formats, and capabilities fail visibly and are
|
|
20
|
+
included in structured results.
|
|
21
|
+
|
|
22
|
+
The current environment includes:
|
|
23
|
+
|
|
24
|
+
- a Bash-like shell with pipelines, redirections, functions, common expansions, control flow,
|
|
25
|
+
background jobs, signals, and job control;
|
|
26
|
+
- common filesystem, text, archive, Git, Make, process, and system commands;
|
|
27
|
+
- deterministic virtual time, network fixtures, `/proc`, `/dev`, processes, descriptors, and
|
|
28
|
+
bounded pipes;
|
|
29
|
+
- a mostly complete Python language runtime with a deliberately selected standard-library and
|
|
30
|
+
third-party module surface.
|
|
31
|
+
|
|
32
|
+
Python is source-compatible where supported, not ABI-compatible with CPython. Native extensions,
|
|
33
|
+
package installation, compilers, and arbitrary machine code are outside the simulation boundary.
|
|
34
|
+
See [Python in shellsim](docs/python.md) for the current contract.
|
|
35
|
+
|
|
36
|
+
## Install and run
|
|
37
|
+
|
|
38
|
+
Install the Python package and console command:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
python -m pip install shellsim
|
|
42
|
+
shellsim -c 'printf "b\na\n" | sort'
|
|
43
|
+
shellsim --root ./project -c 'python3.14 test.py'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`--root` copies the selected host tree into a disposable `/work` snapshot. With no `-c` and a
|
|
47
|
+
terminal attached, `shellsim` starts a persistent interactive session.
|
|
48
|
+
|
|
49
|
+
To build the Rust binaries from source:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
cargo build --release
|
|
53
|
+
./target/release/shellsim -c 'echo hello'
|
|
54
|
+
./target/release/shellsim eval --cpu 100k --memory 8m -c 'make test'
|
|
55
|
+
./target/release/shellsim-python ./project/main.py -- arg1
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
|
|
59
|
+
exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
|
|
60
|
+
|
|
61
|
+
The Python API exposes fresh and persistent environments:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import shellsim
|
|
65
|
+
|
|
66
|
+
environment = shellsim.Environment(cpu=100_000)
|
|
67
|
+
environment.write_file("/work/main.py", "print(6 * 7)\n")
|
|
68
|
+
result = environment.run("python3.14 /work/main.py")
|
|
69
|
+
assert result.returncode == 0
|
|
70
|
+
assert result.stdout == b"42\n"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Agent harness
|
|
74
|
+
|
|
75
|
+
`shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
|
|
76
|
+
bounded execution, streaming actions, VFS operations, checkpoints, workspace diffs, process and
|
|
77
|
+
resource inspection, and deterministic session forks. `shellsim mcp` exposes the same environment
|
|
78
|
+
as a stdio MCP server. `shellsim replay scenario.ndjson` reruns checked action transcripts.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
printf '%s\n' \
|
|
82
|
+
'{"id":1,"op":"execute","source":"printf hello > result"}' \
|
|
83
|
+
'{"id":2,"op":"workspace_diff"}' \
|
|
84
|
+
| shellsim serve --root ./project
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Resource model
|
|
88
|
+
|
|
89
|
+
CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
|
|
90
|
+
usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
|
|
91
|
+
64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
|
|
92
|
+
Exhaustion is observable and never falls back to an ambient host implementation.
|
|
93
|
+
|
|
94
|
+
For internals and contribution workflow, see [implementation](docs/implementation.md),
|
|
95
|
+
[Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -5,7 +5,7 @@ build-backend = "maturin"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shellsim"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "
|
|
8
|
+
description = "In-process BusyBox-like environment for RL rollouts and agent tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = { file = "LICENSE" }
|
|
@@ -22,13 +22,16 @@ classifiers = [
|
|
|
22
22
|
"Topic :: Software Development :: Testing",
|
|
23
23
|
"Typing :: Typed",
|
|
24
24
|
]
|
|
25
|
-
keywords = ["
|
|
25
|
+
keywords = ["agents", "containers", "reinforcement-learning", "sandbox", "simulation", "testing"]
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
28
|
Homepage = "https://github.com/rjpower/shellsim"
|
|
29
29
|
Issues = "https://github.com/rjpower/shellsim/issues"
|
|
30
30
|
Repository = "https://github.com/rjpower/shellsim"
|
|
31
31
|
|
|
32
|
+
[project.scripts]
|
|
33
|
+
shellsim = "shellsim._cli:main"
|
|
34
|
+
|
|
32
35
|
[dependency-groups]
|
|
33
36
|
test = ["pytest>=8"]
|
|
34
37
|
|
|
@@ -42,3 +45,25 @@ strip = true
|
|
|
42
45
|
|
|
43
46
|
[tool.pytest.ini_options]
|
|
44
47
|
testpaths = ["python_tests"]
|
|
48
|
+
|
|
49
|
+
[tool.marin-style]
|
|
50
|
+
checks = ["ruff-check", "ruff-format"]
|
|
51
|
+
include = [
|
|
52
|
+
"infra/*.py",
|
|
53
|
+
"infra/**/*.py",
|
|
54
|
+
"python/**/*.py",
|
|
55
|
+
"python_tests/*.py",
|
|
56
|
+
"python_tests/**/*.py",
|
|
57
|
+
"tools/*.py",
|
|
58
|
+
"tools/**/*.py",
|
|
59
|
+
]
|
|
60
|
+
main_branch = "main"
|
|
61
|
+
ruff_version = "0.14.3"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 120
|
|
65
|
+
target-version = "py39"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "I", "B", "C4"]
|
|
69
|
+
ignore = ["E501"]
|
|
@@ -406,7 +406,7 @@ dependencies = [
|
|
|
406
406
|
|
|
407
407
|
[[package]]
|
|
408
408
|
name = "shellsim"
|
|
409
|
-
version = "0.1.
|
|
409
|
+
version = "0.1.3"
|
|
410
410
|
dependencies = [
|
|
411
411
|
"base64",
|
|
412
412
|
"crc32fast",
|
|
@@ -427,7 +427,7 @@ dependencies = [
|
|
|
427
427
|
|
|
428
428
|
[[package]]
|
|
429
429
|
name = "shellsim-python-bindings"
|
|
430
|
-
version = "0.1.
|
|
430
|
+
version = "0.1.3"
|
|
431
431
|
dependencies = [
|
|
432
432
|
"pyo3",
|
|
433
433
|
"serde",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[package]
|
|
4
4
|
name = "shellsim-python-bindings"
|
|
5
|
-
version = "0.1.
|
|
5
|
+
version = "0.1.3"
|
|
6
6
|
edition = "2021"
|
|
7
7
|
description = "Python bindings for shellsim"
|
|
8
8
|
license = "Apache-2.0"
|
|
@@ -18,3 +18,7 @@ pyo3 = { version = "0.28", features = ["abi3-py39", "extension-module"] }
|
|
|
18
18
|
serde = { version = "1", features = ["derive"] }
|
|
19
19
|
serde_json = "1"
|
|
20
20
|
shellsim = { path = "../.." }
|
|
21
|
+
|
|
22
|
+
[profile.release]
|
|
23
|
+
opt-level = "s"
|
|
24
|
+
lto = false
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import dataclass
|
|
6
5
|
import json
|
|
7
6
|
import os
|
|
8
|
-
from
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Optional, Tuple, Union
|
|
9
10
|
|
|
10
11
|
from . import _native
|
|
11
12
|
|
|
12
|
-
|
|
13
13
|
SimulationError = _native.SimulationError
|
|
14
14
|
|
|
15
15
|
_MAX_U64 = (1 << 64) - 1
|
|
@@ -111,9 +111,7 @@ class RunResult:
|
|
|
111
111
|
if self.returncode != 0:
|
|
112
112
|
diagnostic = self.stderr_text.strip()
|
|
113
113
|
suffix = f": {diagnostic}" if diagnostic else ""
|
|
114
|
-
raise SimulationError(
|
|
115
|
-
f"shellsim action exited with status {self.returncode}{suffix}"
|
|
116
|
-
)
|
|
114
|
+
raise SimulationError(f"shellsim action exited with status {self.returncode}{suffix}")
|
|
117
115
|
|
|
118
116
|
|
|
119
117
|
class Environment:
|
|
@@ -138,11 +136,7 @@ class Environment:
|
|
|
138
136
|
if limits is not None and not isinstance(limits, Limits):
|
|
139
137
|
raise TypeError("limits must be a shellsim.Limits instance")
|
|
140
138
|
resolved = limits or Limits(
|
|
141
|
-
**{
|
|
142
|
-
name: _validate_limit(name, value)
|
|
143
|
-
for name, value in overrides.items()
|
|
144
|
-
if value is not None
|
|
145
|
-
}
|
|
139
|
+
**{name: _validate_limit(name, value) for name, value in overrides.items() if value is not None}
|
|
146
140
|
)
|
|
147
141
|
self._native = _native.NativeEnvironment(
|
|
148
142
|
resolved.cpu,
|
|
@@ -157,9 +151,7 @@ class Environment:
|
|
|
157
151
|
|
|
158
152
|
return bool(self._native.terminated)
|
|
159
153
|
|
|
160
|
-
def run(
|
|
161
|
-
self, source: str, stdin: Union[bytes, bytearray, memoryview] = b""
|
|
162
|
-
) -> RunResult:
|
|
154
|
+
def run(self, source: str, stdin: Union[bytes, bytearray, memoryview] = b"") -> RunResult:
|
|
163
155
|
"""Execute one complete shell action with an explicit input byte stream."""
|
|
164
156
|
|
|
165
157
|
if not isinstance(source, str):
|
|
@@ -204,9 +196,7 @@ class Environment:
|
|
|
204
196
|
raise TypeError("parents must be bool")
|
|
205
197
|
self._native.mkdir(path, parents)
|
|
206
198
|
|
|
207
|
-
def mount(
|
|
208
|
-
self, host_root: Union[str, os.PathLike[str]], destination: str = "/work"
|
|
209
|
-
) -> MountResult:
|
|
199
|
+
def mount(self, host_root: Union[str, os.PathLike[str]], destination: str = "/work") -> MountResult:
|
|
210
200
|
"""Copy an explicitly trusted host directory into the bounded VFS.
|
|
211
201
|
|
|
212
202
|
The walk rejects symlinks and non-regular files, has a 10,000-file limit, and skips
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Command-line interface for the installed shellsim Python package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from ._api import Environment, RunResult, SimulationError
|
|
11
|
+
|
|
12
|
+
_MAX_U64 = (1 << 64) - 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _quantity(value: str) -> int:
|
|
16
|
+
"""Parse a non-negative count with an optional binary k, m, or g suffix."""
|
|
17
|
+
|
|
18
|
+
suffixes = {"k": 1024, "m": 1024**2, "g": 1024**3}
|
|
19
|
+
suffix = value[-1:].lower()
|
|
20
|
+
multiplier = suffixes.get(suffix, 1)
|
|
21
|
+
digits = value[:-1] if suffix in suffixes else value
|
|
22
|
+
try:
|
|
23
|
+
number = int(digits)
|
|
24
|
+
except ValueError as error:
|
|
25
|
+
raise argparse.ArgumentTypeError("expected a count, optionally suffixed with k, m, or g") from error
|
|
26
|
+
result = number * multiplier
|
|
27
|
+
if number < 0 or result > _MAX_U64:
|
|
28
|
+
raise argparse.ArgumentTypeError(f"expected a count between 0 and {_MAX_U64}")
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parser() -> argparse.ArgumentParser:
|
|
33
|
+
"""Build the command-line parser shared by the console and module entry points."""
|
|
34
|
+
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="shellsim",
|
|
37
|
+
description="Run shell commands in a deterministic in-memory simulation.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("-c", "--command", metavar="SOURCE", help="execute one shell action")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--root",
|
|
42
|
+
metavar="DIR",
|
|
43
|
+
help="copy this trusted host directory into the simulated /work tree",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("--cpu", type=_quantity, help="cumulative CPU fuel limit")
|
|
46
|
+
parser.add_argument("--memory", type=_quantity, help="modeled memory limit")
|
|
47
|
+
parser.add_argument("--disk", type=_quantity, help="simulated filesystem limit")
|
|
48
|
+
parser.add_argument("--output", type=_quantity, help="cumulative output limit")
|
|
49
|
+
return parser
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _binary_stream(stream: Any) -> Any:
|
|
53
|
+
"""Return a stream that accepts bytes, including under in-process test doubles."""
|
|
54
|
+
|
|
55
|
+
return getattr(stream, "buffer", stream)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _write_bytes(stream: Any, data: bytes) -> None:
|
|
59
|
+
"""Write exact simulator bytes to a host console stream."""
|
|
60
|
+
|
|
61
|
+
binary = _binary_stream(stream)
|
|
62
|
+
try:
|
|
63
|
+
binary.write(data)
|
|
64
|
+
except TypeError:
|
|
65
|
+
binary.write(data.decode("utf-8", errors="replace"))
|
|
66
|
+
binary.flush()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _emit_result(result: RunResult, stdout: Any, stderr: Any) -> None:
|
|
70
|
+
"""Forward one simulated action's byte-preserving output."""
|
|
71
|
+
|
|
72
|
+
_write_bytes(stdout, result.stdout)
|
|
73
|
+
_write_bytes(stderr, result.stderr)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _execute_action(
|
|
77
|
+
environment: Environment,
|
|
78
|
+
source: str,
|
|
79
|
+
stdin: bytes,
|
|
80
|
+
stdout: Any,
|
|
81
|
+
stderr: Any,
|
|
82
|
+
) -> Optional[RunResult]:
|
|
83
|
+
"""Execute and forward one action, reporting adapter failures without a traceback."""
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = environment.run(source, stdin)
|
|
87
|
+
except SimulationError as error:
|
|
88
|
+
print(f"shellsim: {error}", file=stderr)
|
|
89
|
+
return None
|
|
90
|
+
_emit_result(result, stdout, stderr)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _read_bytes(stream: Any) -> bytes:
|
|
95
|
+
"""Read all remaining input without depending on a text-stream encoding."""
|
|
96
|
+
|
|
97
|
+
data = _binary_stream(stream).read()
|
|
98
|
+
return data.encode() if isinstance(data, str) else data
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _prepare_environment(options: argparse.Namespace, stdout: Any, stderr: Any) -> tuple[Optional[Environment], int]:
|
|
102
|
+
"""Create a simulated machine, import an optional host snapshot, and enter `/work`."""
|
|
103
|
+
|
|
104
|
+
environment = Environment(
|
|
105
|
+
cpu=options.cpu,
|
|
106
|
+
memory=options.memory,
|
|
107
|
+
disk=options.disk,
|
|
108
|
+
output=options.output,
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
if options.root is not None:
|
|
112
|
+
environment.mount(options.root)
|
|
113
|
+
prepared = environment.run("cd /work")
|
|
114
|
+
except SimulationError as error:
|
|
115
|
+
print(f"shellsim: {error}", file=stderr)
|
|
116
|
+
return None, 2
|
|
117
|
+
|
|
118
|
+
if prepared.returncode != 0:
|
|
119
|
+
_emit_result(prepared, stdout, stderr)
|
|
120
|
+
return None, prepared.returncode
|
|
121
|
+
return environment, 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _interactive(environment: Environment, stdin: Any, stdout: Any, stderr: Any) -> int:
|
|
125
|
+
"""Run one action per input line while retaining simulated state."""
|
|
126
|
+
|
|
127
|
+
status = 0
|
|
128
|
+
while not environment.terminated:
|
|
129
|
+
stdout.write("shellsim$ ")
|
|
130
|
+
stdout.flush()
|
|
131
|
+
try:
|
|
132
|
+
line = stdin.readline()
|
|
133
|
+
except KeyboardInterrupt:
|
|
134
|
+
stdout.write("\n")
|
|
135
|
+
stdout.flush()
|
|
136
|
+
continue
|
|
137
|
+
if not line:
|
|
138
|
+
stdout.write("\n")
|
|
139
|
+
stdout.flush()
|
|
140
|
+
break
|
|
141
|
+
result = _execute_action(environment, line, b"", stdout, stderr)
|
|
142
|
+
if result is None:
|
|
143
|
+
return 1
|
|
144
|
+
status = result.returncode
|
|
145
|
+
return status
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(arguments: Optional[Sequence[str]] = None) -> int:
|
|
149
|
+
"""Run the package CLI and return the simulated or usage exit status."""
|
|
150
|
+
|
|
151
|
+
options = _parser().parse_args(sys.argv[1:] if arguments is None else arguments)
|
|
152
|
+
environment, preparation_status = _prepare_environment(options, sys.stdout, sys.stderr)
|
|
153
|
+
if environment is None:
|
|
154
|
+
return preparation_status
|
|
155
|
+
|
|
156
|
+
if options.command is not None:
|
|
157
|
+
stdin = b"" if sys.stdin.isatty() else _read_bytes(sys.stdin)
|
|
158
|
+
result = _execute_action(environment, options.command, stdin, sys.stdout, sys.stderr)
|
|
159
|
+
if result is None:
|
|
160
|
+
return 1
|
|
161
|
+
return result.returncode
|
|
162
|
+
|
|
163
|
+
if sys.stdin.isatty():
|
|
164
|
+
return _interactive(environment, sys.stdin, sys.stdout, sys.stderr)
|
|
165
|
+
|
|
166
|
+
source_bytes = _read_bytes(sys.stdin)
|
|
167
|
+
try:
|
|
168
|
+
source = source_bytes.decode("utf-8")
|
|
169
|
+
except UnicodeDecodeError as error:
|
|
170
|
+
print(f"shellsim: stdin is not valid UTF-8 shell source: {error}", file=sys.stderr)
|
|
171
|
+
return 2
|
|
172
|
+
result = _execute_action(environment, source, b"", sys.stdout, sys.stderr)
|
|
173
|
+
if result is None:
|
|
174
|
+
return 1
|
|
175
|
+
return result.returncode
|