shellsim 0.1.3__tar.gz → 0.1.5__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.3 → shellsim-0.1.5}/Cargo.lock +1 -1
- {shellsim-0.1.3 → shellsim-0.1.5}/Cargo.toml +4 -3
- {shellsim-0.1.3 → shellsim-0.1.5}/PKG-INFO +52 -3
- {shellsim-0.1.3 → shellsim-0.1.5}/README.md +51 -2
- {shellsim-0.1.3 → shellsim-0.1.5}/pyproject.toml +4 -3
- {shellsim-0.1.3 → shellsim-0.1.5}/python/native/Cargo.lock +2 -2
- {shellsim-0.1.3 → shellsim-0.1.5}/python/native/Cargo.toml +4 -3
- {shellsim-0.1.3 → shellsim-0.1.5}/python/native/src/lib.rs +128 -17
- {shellsim-0.1.3 → shellsim-0.1.5}/python/shellsim/__init__.py +6 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/python/shellsim/_api.py +121 -3
- shellsim-0.1.5/python/shellsim/python.py +32 -0
- shellsim-0.1.5/src/commands/awk/ast.rs +146 -0
- shellsim-0.1.5/src/commands/awk/eval.rs +1180 -0
- shellsim-0.1.5/src/commands/awk/lexer.rs +367 -0
- shellsim-0.1.5/src/commands/awk/parser.rs +522 -0
- shellsim-0.1.5/src/commands/awk.rs +243 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/builtins.rs +155 -55
- shellsim-0.1.5/src/commands/dd.rs +308 -0
- shellsim-0.1.5/src/commands/find.rs +323 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/fs.rs +47 -172
- shellsim-0.1.5/src/commands/git/apply.rs +255 -0
- shellsim-0.1.5/src/commands/git/commit.rs +339 -0
- shellsim-0.1.5/src/commands/git/compare.rs +731 -0
- shellsim-0.1.5/src/commands/git/config.rs +356 -0
- shellsim-0.1.5/src/commands/git/conflict.rs +564 -0
- shellsim-0.1.5/src/commands/git/diff.rs +609 -0
- shellsim-0.1.5/src/commands/git/ignore.rs +381 -0
- shellsim-0.1.5/src/commands/git/import.rs +655 -0
- shellsim-0.1.5/src/commands/git/log.rs +1055 -0
- shellsim-0.1.5/src/commands/git/merge.rs +722 -0
- shellsim-0.1.5/src/commands/git/mod.rs +795 -0
- shellsim-0.1.5/src/commands/git/plumbing.rs +1215 -0
- shellsim-0.1.5/src/commands/git/rebase.rs +475 -0
- shellsim-0.1.5/src/commands/git/refs.rs +699 -0
- shellsim-0.1.5/src/commands/git/repo.rs +1206 -0
- shellsim-0.1.5/src/commands/git/stash.rs +441 -0
- shellsim-0.1.5/src/commands/git/switch.rs +522 -0
- shellsim-0.1.5/src/commands/git/worktree.rs +1592 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/mod.rs +57 -7
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/net.rs +14 -4
- shellsim-0.1.5/src/commands/options.rs +271 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/patch.rs +100 -23
- shellsim-0.1.5/src/commands/pkg.rs +304 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/proc.rs +220 -92
- shellsim-0.1.5/src/commands/regex_compat.rs +56 -0
- shellsim-0.1.5/src/commands/sed.rs +787 -0
- shellsim-0.1.5/src/commands/sort.rs +255 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/system.rs +1 -1
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/text.rs +389 -385
- shellsim-0.1.5/src/commands/unavailable.rs +39 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/util.rs +24 -1
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/zipcmd.rs +12 -1
- {shellsim-0.1.3 → shellsim-0.1.5}/src/exec.rs +108 -7
- {shellsim-0.1.3 → shellsim-0.1.5}/src/harness.rs +3 -3
- {shellsim-0.1.3 → shellsim-0.1.5}/src/host_ingest.rs +62 -6
- {shellsim-0.1.3 → shellsim-0.1.5}/src/interp.rs +37 -9
- shellsim-0.1.5/src/jqcmd.rs +1497 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/main.rs +7 -4
- shellsim-0.1.5/src/net.rs +550 -0
- shellsim-0.1.5/src/netcmd.rs +357 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/ast.rs +16 -3
- shellsim-0.1.5/src/python/bytecode.rs +540 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/compiler.rs +304 -75
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/filesystem.rs +81 -32
- shellsim-0.1.5/src/python/heap.rs +1843 -0
- shellsim-0.1.5/src/python/http.rs +135 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/lexer.rs +48 -3
- shellsim-0.1.5/src/python/mapping.rs +180 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/mod.rs +282 -48
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/native.rs +152 -51
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/number.rs +266 -80
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/object_model.rs +156 -77
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/parser.rs +310 -92
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/protocol.rs +248 -69
- shellsim-0.1.5/src/python/slice.rs +133 -0
- shellsim-0.1.5/src/python/stdlib/argparse.rs +674 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/core.rs +647 -131
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/frozen.rs +10 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/hashlib.rs +3 -2
- shellsim-0.1.5/src/python/stdlib/http.rs +63 -0
- shellsim-0.1.5/src/python/stdlib/importlib.rs +43 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/json.rs +39 -8
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/math.rs +36 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/mod.rs +4 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/numpy.rs +858 -79
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/os.rs +19 -10
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/re.rs +3 -3
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/csv.py +4 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/datetime.py +15 -0
- shellsim-0.1.5/src/python/stdlib/source/http.py +3 -0
- shellsim-0.1.5/src/python/stdlib/source/http_client.py +271 -0
- shellsim-0.1.5/src/python/stdlib/source/importlib.py +47 -0
- shellsim-0.1.5/src/python/stdlib/source/json.py +25 -0
- shellsim-0.1.5/src/python/stdlib/source/numpy_random.py +99 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/os.py +81 -8
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/pathlib.py +36 -0
- shellsim-0.1.5/src/python/stdlib/source/random.py +97 -0
- shellsim-0.1.5/src/python/stdlib/source/statistics.py +127 -0
- shellsim-0.1.5/src/python/stdlib/source/urllib.py +4 -0
- shellsim-0.1.5/src/python/stdlib/source/urllib_error.py +50 -0
- shellsim-0.1.5/src/python/stdlib/source/urllib_request.py +155 -0
- shellsim-0.1.5/src/python/stdlib/source/zipfile.py +234 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/struct.rs +5 -5
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/subprocess.rs +6 -6
- shellsim-0.1.5/src/python/stdlib/sys.rs +183 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/vfs.rs +65 -18
- shellsim-0.1.5/src/python/string.rs +170 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/token.rs +5 -0
- shellsim-0.1.5/src/python/vm/calls.rs +1148 -0
- shellsim-0.1.5/src/python/vm/dispatch.rs +641 -0
- shellsim-0.1.5/src/python/vm/host.rs +175 -0
- shellsim-0.1.5/src/python/vm/iteration.rs +359 -0
- shellsim-0.1.5/src/python/vm/namespace.rs +652 -0
- shellsim-0.1.5/src/python/vm/native_runtime.rs +1678 -0
- shellsim-0.1.5/src/python/vm/objects.rs +1994 -0
- shellsim-0.1.5/src/python/vm/operations.rs +420 -0
- shellsim-0.1.5/src/python/vm.rs +1275 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/resources.rs +5 -0
- shellsim-0.1.5/src/sandbox.rs +291 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/scenario.rs +5 -5
- {shellsim-0.1.3 → shellsim-0.1.5}/src/shell.rs +142 -12
- {shellsim-0.1.3 → shellsim-0.1.5}/src/telemetry.rs +2 -2
- shellsim-0.1.3/src/commands/awk.rs +0 -674
- shellsim-0.1.3/src/commands/git.rs +0 -1858
- shellsim-0.1.3/src/commands/pkg.rs +0 -223
- shellsim-0.1.3/src/commands/sort.rs +0 -146
- shellsim-0.1.3/src/jqcmd.rs +0 -352
- shellsim-0.1.3/src/net.rs +0 -218
- shellsim-0.1.3/src/netcmd.rs +0 -135
- shellsim-0.1.3/src/python/bytecode.rs +0 -107
- shellsim-0.1.3/src/python/heap.rs +0 -586
- shellsim-0.1.3/src/python/stdlib/argparse.rs +0 -265
- shellsim-0.1.3/src/python/stdlib/source/json.py +0 -15
- shellsim-0.1.3/src/python/stdlib/sys.rs +0 -89
- shellsim-0.1.3/src/python/vm.rs +0 -6395
- shellsim-0.1.3/src/sandbox.rs +0 -74
- {shellsim-0.1.3 → shellsim-0.1.5}/LICENSE +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/python/shellsim/__main__.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/python/shellsim/_cli.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/python/shellsim/py.typed +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/rust-toolchain.toml +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/bin/shellsim-python.rs +1 -1
- {shellsim-0.1.3 → shellsim-0.1.5}/src/clock.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/archives.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/echo.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/hashing.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/makecmd.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/printf.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/rgcmd.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/commands/tarcmd.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/descriptors.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/expand.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/harness_manager.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/hashes.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/lib.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/mcp.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/process.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/pseudo_fs.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/process.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/source.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/base64.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/bisect.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/collections.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/dataclasses.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/enum.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/functools.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/heapq.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/itertools.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/pytest.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/abc.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/base64.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/codecs.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/collections.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/glob.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/hashlib.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/io.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/logging.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/struct.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/subprocess.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/tempfile.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/uuid.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/source/zlib.py +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/string.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/time.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/typing.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/unittest.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/python/stdlib/zlib.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/scheduler.rs +0 -0
- {shellsim-0.1.3 → shellsim-0.1.5}/src/vfs.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "shellsim"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.5"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "In-process BusyBox-like environment for RL rollouts and agent tests"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -58,5 +58,6 @@ python = []
|
|
|
58
58
|
opt-level = 1
|
|
59
59
|
|
|
60
60
|
[profile.release]
|
|
61
|
-
opt-level = "
|
|
62
|
-
lto =
|
|
61
|
+
opt-level = "z"
|
|
62
|
+
lto = "fat"
|
|
63
|
+
codegen-units = 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shellsim
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
6
6
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
@@ -53,8 +53,25 @@ The current environment includes:
|
|
|
53
53
|
third-party module surface.
|
|
54
54
|
|
|
55
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
|
|
57
|
-
|
|
56
|
+
network package installation, compilers, and arbitrary machine code are outside the simulation
|
|
57
|
+
boundary. The simulated `pip` and `uv` paths can activate packages already bundled with
|
|
58
|
+
shellsim; they reject other packages instead of fetching them. See
|
|
59
|
+
[Python in shellsim](docs/python.md) for the current contract.
|
|
60
|
+
|
|
61
|
+
`git` provides a porcelain subset backed by the virtual filesystem: staging, commits, history,
|
|
62
|
+
diffs, branches, tags, merges, stashes, and search, with output that matches real Git where an
|
|
63
|
+
agent is likely to parse it. Networked subcommands and content conflicts are refused rather than
|
|
64
|
+
approximated. See [Git in shellsim](docs/git.md) for the supported surface and its boundaries.
|
|
65
|
+
|
|
66
|
+
Common utilities accept clustered short options, long options, option values, and `--` through a
|
|
67
|
+
shared parser. Each utility declares its supported options. For example, `grep` supports ordinary
|
|
68
|
+
basic and extended regular expressions, recursive search, fixed strings, word and whole-line
|
|
69
|
+
matching, pattern files, include and exclude filters, context, counts, line numbers, match limits,
|
|
70
|
+
and common output controls. `sed` covers addresses and ranges plus the common substitution,
|
|
71
|
+
selection, text, transliteration, and early-exit commands. `awk` parses a typed language subset
|
|
72
|
+
with record rules, control flow, fields, associative arrays, arithmetic, regular expressions, and
|
|
73
|
+
the usual scalar functions. Unsupported syntax and options exit nonzero with a direct diagnostic;
|
|
74
|
+
text processing is UTF-8-only unless a command documents a byte-oriented mode.
|
|
58
75
|
|
|
59
76
|
## Install and run
|
|
60
77
|
|
|
@@ -80,6 +97,8 @@ cargo build --release
|
|
|
80
97
|
|
|
81
98
|
Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
|
|
82
99
|
exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
|
|
100
|
+
`unsupported_commands` lists recognized or attempted commands that crossed the capability
|
|
101
|
+
boundary; invocation records include trust, status, and the unsupported reason.
|
|
83
102
|
|
|
84
103
|
The Python API exposes fresh and persistent environments:
|
|
85
104
|
|
|
@@ -93,6 +112,36 @@ assert result.returncode == 0
|
|
|
93
112
|
assert result.stdout == b"42\n"
|
|
94
113
|
```
|
|
95
114
|
|
|
115
|
+
Static HTTP fixtures use the same isolated environment. They do not enable sockets, DNS, TLS, or
|
|
116
|
+
host-network access:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
environment = shellsim.Environment(http={
|
|
120
|
+
"https://api.test/items/*": shellsim.HttpResponse(
|
|
121
|
+
status=200,
|
|
122
|
+
headers={"Content-Type": "application/json"},
|
|
123
|
+
body='{"items": []}',
|
|
124
|
+
),
|
|
125
|
+
})
|
|
126
|
+
result = environment.run("curl -s https://api.test/items/1")
|
|
127
|
+
|
|
128
|
+
result = environment.run_python("""
|
|
129
|
+
from urllib.request import urlopen
|
|
130
|
+
print(urlopen('https://api.test/items/1').read())
|
|
131
|
+
""")
|
|
132
|
+
assert result.network_requests[0].matched
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Python source can bypass shell parsing and quoting while using the same isolated runtime:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
result = shellsim.python.run("print(sum(range(10)))", argv=["example"])
|
|
139
|
+
result.check_returncode()
|
|
140
|
+
|
|
141
|
+
environment = shellsim.Environment()
|
|
142
|
+
result = environment.run_python("print('persistent VFS, fresh Python interpreter')")
|
|
143
|
+
```
|
|
144
|
+
|
|
96
145
|
## Agent harness
|
|
97
146
|
|
|
98
147
|
`shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
|
|
@@ -30,8 +30,25 @@ The current environment includes:
|
|
|
30
30
|
third-party module surface.
|
|
31
31
|
|
|
32
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
|
|
34
|
-
|
|
33
|
+
network package installation, compilers, and arbitrary machine code are outside the simulation
|
|
34
|
+
boundary. The simulated `pip` and `uv` paths can activate packages already bundled with
|
|
35
|
+
shellsim; they reject other packages instead of fetching them. See
|
|
36
|
+
[Python in shellsim](docs/python.md) for the current contract.
|
|
37
|
+
|
|
38
|
+
`git` provides a porcelain subset backed by the virtual filesystem: staging, commits, history,
|
|
39
|
+
diffs, branches, tags, merges, stashes, and search, with output that matches real Git where an
|
|
40
|
+
agent is likely to parse it. Networked subcommands and content conflicts are refused rather than
|
|
41
|
+
approximated. See [Git in shellsim](docs/git.md) for the supported surface and its boundaries.
|
|
42
|
+
|
|
43
|
+
Common utilities accept clustered short options, long options, option values, and `--` through a
|
|
44
|
+
shared parser. Each utility declares its supported options. For example, `grep` supports ordinary
|
|
45
|
+
basic and extended regular expressions, recursive search, fixed strings, word and whole-line
|
|
46
|
+
matching, pattern files, include and exclude filters, context, counts, line numbers, match limits,
|
|
47
|
+
and common output controls. `sed` covers addresses and ranges plus the common substitution,
|
|
48
|
+
selection, text, transliteration, and early-exit commands. `awk` parses a typed language subset
|
|
49
|
+
with record rules, control flow, fields, associative arrays, arithmetic, regular expressions, and
|
|
50
|
+
the usual scalar functions. Unsupported syntax and options exit nonzero with a direct diagnostic;
|
|
51
|
+
text processing is UTF-8-only unless a command documents a byte-oriented mode.
|
|
35
52
|
|
|
36
53
|
## Install and run
|
|
37
54
|
|
|
@@ -57,6 +74,8 @@ cargo build --release
|
|
|
57
74
|
|
|
58
75
|
Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
|
|
59
76
|
exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
|
|
77
|
+
`unsupported_commands` lists recognized or attempted commands that crossed the capability
|
|
78
|
+
boundary; invocation records include trust, status, and the unsupported reason.
|
|
60
79
|
|
|
61
80
|
The Python API exposes fresh and persistent environments:
|
|
62
81
|
|
|
@@ -70,6 +89,36 @@ assert result.returncode == 0
|
|
|
70
89
|
assert result.stdout == b"42\n"
|
|
71
90
|
```
|
|
72
91
|
|
|
92
|
+
Static HTTP fixtures use the same isolated environment. They do not enable sockets, DNS, TLS, or
|
|
93
|
+
host-network access:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
environment = shellsim.Environment(http={
|
|
97
|
+
"https://api.test/items/*": shellsim.HttpResponse(
|
|
98
|
+
status=200,
|
|
99
|
+
headers={"Content-Type": "application/json"},
|
|
100
|
+
body='{"items": []}',
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
result = environment.run("curl -s https://api.test/items/1")
|
|
104
|
+
|
|
105
|
+
result = environment.run_python("""
|
|
106
|
+
from urllib.request import urlopen
|
|
107
|
+
print(urlopen('https://api.test/items/1').read())
|
|
108
|
+
""")
|
|
109
|
+
assert result.network_requests[0].matched
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Python source can bypass shell parsing and quoting while using the same isolated runtime:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
result = shellsim.python.run("print(sum(range(10)))", argv=["example"])
|
|
116
|
+
result.check_returncode()
|
|
117
|
+
|
|
118
|
+
environment = shellsim.Environment()
|
|
119
|
+
result = environment.run_python("print('persistent VFS, fresh Python interpreter')")
|
|
120
|
+
```
|
|
121
|
+
|
|
73
122
|
## Agent harness
|
|
74
123
|
|
|
75
124
|
`shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
|
|
@@ -44,7 +44,7 @@ locked = true
|
|
|
44
44
|
strip = true
|
|
45
45
|
|
|
46
46
|
[tool.pytest.ini_options]
|
|
47
|
-
testpaths = ["
|
|
47
|
+
testpaths = ["tests/python_package", "tests/tooling"]
|
|
48
48
|
|
|
49
49
|
[tool.marin-style]
|
|
50
50
|
checks = ["ruff-check", "ruff-format"]
|
|
@@ -52,8 +52,9 @@ include = [
|
|
|
52
52
|
"infra/*.py",
|
|
53
53
|
"infra/**/*.py",
|
|
54
54
|
"python/**/*.py",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
55
|
+
"tests/python/test_*.py",
|
|
56
|
+
"tests/python_package/**/*.py",
|
|
57
|
+
"tests/tooling/**/*.py",
|
|
57
58
|
"tools/*.py",
|
|
58
59
|
"tools/**/*.py",
|
|
59
60
|
]
|
|
@@ -406,7 +406,7 @@ dependencies = [
|
|
|
406
406
|
|
|
407
407
|
[[package]]
|
|
408
408
|
name = "shellsim"
|
|
409
|
-
version = "0.1.
|
|
409
|
+
version = "0.1.5"
|
|
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.5"
|
|
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.5"
|
|
6
6
|
edition = "2021"
|
|
7
7
|
description = "Python bindings for shellsim"
|
|
8
8
|
license = "Apache-2.0"
|
|
@@ -20,5 +20,6 @@ serde_json = "1"
|
|
|
20
20
|
shellsim = { path = "../.." }
|
|
21
21
|
|
|
22
22
|
[profile.release]
|
|
23
|
-
opt-level = "
|
|
24
|
-
lto =
|
|
23
|
+
opt-level = "z"
|
|
24
|
+
lto = "fat"
|
|
25
|
+
codegen-units = 1
|
|
@@ -17,6 +17,7 @@ use pyo3::exceptions::PyException;
|
|
|
17
17
|
use pyo3::prelude::*;
|
|
18
18
|
use pyo3::types::PyBytes;
|
|
19
19
|
use serde::Serialize;
|
|
20
|
+
use shellsim::net::NetworkRequest;
|
|
20
21
|
use shellsim::{CommandTrust, InvocationEvent, Limits, RunOutcome};
|
|
21
22
|
|
|
22
23
|
const ACTION_STACK_BYTES: usize = 8 * 1024 * 1024;
|
|
@@ -35,10 +36,12 @@ struct RunMetadata {
|
|
|
35
36
|
dropped_unsupported: u64,
|
|
36
37
|
commands: Vec<String>,
|
|
37
38
|
dropped_commands: u64,
|
|
38
|
-
|
|
39
|
+
unsupported_commands: Vec<String>,
|
|
39
40
|
partial_commands: Vec<String>,
|
|
40
41
|
invocations: Vec<InvocationEvent>,
|
|
41
42
|
dropped_invocations: u64,
|
|
43
|
+
network_requests: Vec<NetworkRequest>,
|
|
44
|
+
dropped_network_requests: u64,
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
#[derive(Serialize)]
|
|
@@ -47,6 +50,17 @@ struct MountMetadata {
|
|
|
47
50
|
skipped_directories: Vec<String>,
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
struct MetadataStart {
|
|
54
|
+
command: usize,
|
|
55
|
+
dropped_commands: u64,
|
|
56
|
+
unsupported: usize,
|
|
57
|
+
dropped_unsupported: u64,
|
|
58
|
+
invocation: u64,
|
|
59
|
+
dropped_invocations: u64,
|
|
60
|
+
network: usize,
|
|
61
|
+
dropped_network: u64,
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
/// One persistent simulated machine owned by Python.
|
|
51
65
|
#[pyclass(module = "shellsim._native")]
|
|
52
66
|
struct NativeEnvironment {
|
|
@@ -91,6 +105,31 @@ impl NativeEnvironment {
|
|
|
91
105
|
))
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
/// Execute Python source directly without passing it through the shell parser.
|
|
109
|
+
fn run_python(
|
|
110
|
+
&self,
|
|
111
|
+
py: Python<'_>,
|
|
112
|
+
source: String,
|
|
113
|
+
argv: Vec<String>,
|
|
114
|
+
stdin: Vec<u8>,
|
|
115
|
+
) -> PyResult<(String, Py<PyBytes>, Py<PyBytes>)> {
|
|
116
|
+
let (metadata, stdout, stderr) = py
|
|
117
|
+
.detach(|| {
|
|
118
|
+
let mut environment = self.lock_environment()?;
|
|
119
|
+
on_worker(&mut environment, move |environment| {
|
|
120
|
+
Ok(run_python_action(environment, source, argv, stdin))
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
.map_err(SimulationError::new_err)?;
|
|
124
|
+
let metadata = serde_json::to_string(&metadata)
|
|
125
|
+
.map_err(|error| SimulationError::new_err(error.to_string()))?;
|
|
126
|
+
Ok((
|
|
127
|
+
metadata,
|
|
128
|
+
PyBytes::new(py, &stdout).unbind(),
|
|
129
|
+
PyBytes::new(py, &stderr).unbind(),
|
|
130
|
+
))
|
|
131
|
+
}
|
|
132
|
+
|
|
94
133
|
/// Write one byte string into the simulated filesystem.
|
|
95
134
|
fn write_file(&self, py: Python<'_>, path: String, data: Vec<u8>, mode: u32) -> PyResult<()> {
|
|
96
135
|
py.detach(|| {
|
|
@@ -139,6 +178,36 @@ impl NativeEnvironment {
|
|
|
139
178
|
.map_err(SimulationError::new_err)
|
|
140
179
|
}
|
|
141
180
|
|
|
181
|
+
/// Register one static response in the simulated HTTP broker.
|
|
182
|
+
fn route_http(
|
|
183
|
+
&self,
|
|
184
|
+
py: Python<'_>,
|
|
185
|
+
pattern: String,
|
|
186
|
+
method: Option<String>,
|
|
187
|
+
status: u16,
|
|
188
|
+
headers: Vec<(String, String)>,
|
|
189
|
+
body: Vec<u8>,
|
|
190
|
+
) -> PyResult<()> {
|
|
191
|
+
py.detach(|| {
|
|
192
|
+
let mut environment = self.lock_environment()?;
|
|
193
|
+
on_worker(&mut environment, move |environment| {
|
|
194
|
+
environment
|
|
195
|
+
.net
|
|
196
|
+
.route(
|
|
197
|
+
&pattern,
|
|
198
|
+
method.as_deref(),
|
|
199
|
+
shellsim::net::HttpResponse {
|
|
200
|
+
status,
|
|
201
|
+
headers,
|
|
202
|
+
body,
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
.map_err(|error| error.to_string())
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
.map_err(SimulationError::new_err)
|
|
209
|
+
}
|
|
210
|
+
|
|
142
211
|
/// Import one explicitly selected trusted host tree into the simulated filesystem.
|
|
143
212
|
fn mount(
|
|
144
213
|
&self,
|
|
@@ -188,35 +257,77 @@ fn run_action(
|
|
|
188
257
|
source: &str,
|
|
189
258
|
stdin: &[u8],
|
|
190
259
|
) -> (RunMetadata, Vec<u8>, Vec<u8>) {
|
|
191
|
-
let
|
|
192
|
-
let dropped_commands_start = environment.cmd_trace.dropped();
|
|
193
|
-
let unsupported_start = environment.unsupported.len();
|
|
194
|
-
let dropped_unsupported_start = environment.unsupported.dropped();
|
|
195
|
-
let invocation_start = environment.invocations.next_sequence();
|
|
196
|
-
let dropped_invocations_start = environment.invocations.dropped();
|
|
260
|
+
let start = metadata_start(environment);
|
|
197
261
|
let (outcome, stdout, stderr) = environment.run_script_capture_with_stdin(source, stdin);
|
|
198
|
-
|
|
199
|
-
|
|
262
|
+
(metadata_finish(environment, outcome, start), stdout, stderr)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn run_python_action(
|
|
266
|
+
environment: &mut shellsim::Environment,
|
|
267
|
+
source: String,
|
|
268
|
+
arguments: Vec<String>,
|
|
269
|
+
stdin: Vec<u8>,
|
|
270
|
+
) -> (RunMetadata, Vec<u8>, Vec<u8>) {
|
|
271
|
+
let start = metadata_start(environment);
|
|
272
|
+
let mut argv = vec!["python3.14".to_string(), "-c".to_string(), source];
|
|
273
|
+
argv.extend(arguments);
|
|
274
|
+
let mut stdout = Vec::new();
|
|
275
|
+
let mut stderr = Vec::new();
|
|
276
|
+
let status = shellsim::exec::exec(
|
|
277
|
+
environment,
|
|
278
|
+
&shellsim::shell::Node::ArgvCommand(argv),
|
|
279
|
+
stdin,
|
|
280
|
+
&mut stdout,
|
|
281
|
+
&mut stderr,
|
|
282
|
+
);
|
|
283
|
+
let outcome = environment.outcome(status);
|
|
284
|
+
(metadata_finish(environment, outcome, start), stdout, stderr)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn metadata_start(environment: &shellsim::Environment) -> MetadataStart {
|
|
288
|
+
MetadataStart {
|
|
289
|
+
command: environment.cmd_trace.len(),
|
|
290
|
+
dropped_commands: environment.cmd_trace.dropped(),
|
|
291
|
+
unsupported: environment.unsupported.len(),
|
|
292
|
+
dropped_unsupported: environment.unsupported.dropped(),
|
|
293
|
+
invocation: environment.invocations.next_sequence(),
|
|
294
|
+
dropped_invocations: environment.invocations.dropped(),
|
|
295
|
+
network: environment.net.log.len(),
|
|
296
|
+
dropped_network: environment.net.dropped_requests,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn metadata_finish(
|
|
301
|
+
environment: &shellsim::Environment,
|
|
302
|
+
outcome: RunOutcome,
|
|
303
|
+
start: MetadataStart,
|
|
304
|
+
) -> RunMetadata {
|
|
305
|
+
let invocations = environment.invocations.events_since(start.invocation);
|
|
306
|
+
RunMetadata {
|
|
200
307
|
outcome,
|
|
201
|
-
unsupported: environment.unsupported.values_since(
|
|
308
|
+
unsupported: environment.unsupported.values_since(start.unsupported),
|
|
202
309
|
dropped_unsupported: environment
|
|
203
310
|
.unsupported
|
|
204
311
|
.dropped()
|
|
205
|
-
.saturating_sub(
|
|
206
|
-
commands: environment.cmd_trace.values_since(
|
|
312
|
+
.saturating_sub(start.dropped_unsupported),
|
|
313
|
+
commands: environment.cmd_trace.values_since(start.command),
|
|
207
314
|
dropped_commands: environment
|
|
208
315
|
.cmd_trace
|
|
209
316
|
.dropped()
|
|
210
|
-
.saturating_sub(
|
|
211
|
-
|
|
317
|
+
.saturating_sub(start.dropped_commands),
|
|
318
|
+
unsupported_commands: invocation_names(&invocations, CommandTrust::Unsupported),
|
|
212
319
|
partial_commands: invocation_names(&invocations, CommandTrust::Partial),
|
|
213
320
|
invocations,
|
|
214
321
|
dropped_invocations: environment
|
|
215
322
|
.invocations
|
|
216
323
|
.dropped()
|
|
217
|
-
.saturating_sub(
|
|
218
|
-
|
|
219
|
-
|
|
324
|
+
.saturating_sub(start.dropped_invocations),
|
|
325
|
+
network_requests: environment.net.log[start.network..].to_vec(),
|
|
326
|
+
dropped_network_requests: environment
|
|
327
|
+
.net
|
|
328
|
+
.dropped_requests
|
|
329
|
+
.saturating_sub(start.dropped_network),
|
|
330
|
+
}
|
|
220
331
|
}
|
|
221
332
|
|
|
222
333
|
fn invocation_names(events: &[InvocationEvent], trust: CommandTrust) -> Vec<String> {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Typed Python interface to shellsim's deterministic execution environment."""
|
|
2
2
|
|
|
3
|
+
from . import python as python
|
|
3
4
|
from ._api import (
|
|
4
5
|
CommandUsage,
|
|
5
6
|
Environment,
|
|
7
|
+
HttpRequest,
|
|
8
|
+
HttpResponse,
|
|
6
9
|
Invocation,
|
|
7
10
|
Limits,
|
|
8
11
|
MountResult,
|
|
@@ -15,6 +18,8 @@ from ._api import (
|
|
|
15
18
|
__all__ = [
|
|
16
19
|
"CommandUsage",
|
|
17
20
|
"Environment",
|
|
21
|
+
"HttpRequest",
|
|
22
|
+
"HttpResponse",
|
|
18
23
|
"Invocation",
|
|
19
24
|
"Limits",
|
|
20
25
|
"MountResult",
|
|
@@ -22,4 +27,5 @@ __all__ = [
|
|
|
22
27
|
"SimulationError",
|
|
23
28
|
"Usage",
|
|
24
29
|
"run",
|
|
30
|
+
"python",
|
|
25
31
|
]
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
-
from collections.abc import Mapping
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import Any, Optional, Tuple, Union
|
|
10
10
|
|
|
@@ -64,6 +64,19 @@ class Invocation:
|
|
|
64
64
|
unsupported_reason: Optional[str] = None
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class HttpRequest:
|
|
69
|
+
"""One bounded request record emitted by the simulated HTTP broker."""
|
|
70
|
+
|
|
71
|
+
method: str
|
|
72
|
+
url: str
|
|
73
|
+
headers: Tuple[Tuple[str, str], ...]
|
|
74
|
+
dropped_headers: int
|
|
75
|
+
body_bytes: int
|
|
76
|
+
matched: bool
|
|
77
|
+
response_status: Optional[int]
|
|
78
|
+
|
|
79
|
+
|
|
67
80
|
@dataclass(frozen=True)
|
|
68
81
|
class MountResult:
|
|
69
82
|
"""Result of copying an explicitly trusted host tree into the VFS."""
|
|
@@ -72,6 +85,15 @@ class MountResult:
|
|
|
72
85
|
skipped_directories: Tuple[str, ...]
|
|
73
86
|
|
|
74
87
|
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class HttpResponse:
|
|
90
|
+
"""One static response returned by the simulated HTTP broker."""
|
|
91
|
+
|
|
92
|
+
status: int = 200
|
|
93
|
+
headers: Union[Mapping[str, str], Sequence[Tuple[str, str]]] = ()
|
|
94
|
+
body: Union[bytes, bytearray, memoryview, str] = b""
|
|
95
|
+
|
|
96
|
+
|
|
75
97
|
@dataclass(frozen=True)
|
|
76
98
|
class RunResult:
|
|
77
99
|
"""Byte-preserving output and structured fidelity/resource telemetry for one action."""
|
|
@@ -88,10 +110,12 @@ class RunResult:
|
|
|
88
110
|
dropped_unsupported: int
|
|
89
111
|
commands: Tuple[str, ...]
|
|
90
112
|
dropped_commands: int
|
|
91
|
-
|
|
113
|
+
unsupported_commands: Tuple[str, ...]
|
|
92
114
|
partial_commands: Tuple[str, ...]
|
|
93
115
|
invocations: Tuple[Invocation, ...]
|
|
94
116
|
dropped_invocations: int
|
|
117
|
+
network_requests: Tuple[HttpRequest, ...]
|
|
118
|
+
dropped_network_requests: int
|
|
95
119
|
|
|
96
120
|
@property
|
|
97
121
|
def stdout_text(self) -> str:
|
|
@@ -129,6 +153,7 @@ class Environment:
|
|
|
129
153
|
memory: Optional[int] = None,
|
|
130
154
|
disk: Optional[int] = None,
|
|
131
155
|
output: Optional[int] = None,
|
|
156
|
+
http: Optional[Mapping[str, HttpResponse]] = None,
|
|
132
157
|
) -> None:
|
|
133
158
|
overrides = {"cpu": cpu, "memory": memory, "disk": disk, "output": output}
|
|
134
159
|
if limits is not None and any(value is not None for value in overrides.values()):
|
|
@@ -144,6 +169,11 @@ class Environment:
|
|
|
144
169
|
resolved.disk,
|
|
145
170
|
resolved.output,
|
|
146
171
|
)
|
|
172
|
+
if http is not None:
|
|
173
|
+
if not isinstance(http, Mapping):
|
|
174
|
+
raise TypeError("http must be a mapping from URL patterns to shellsim.HttpResponse")
|
|
175
|
+
for pattern, response in http.items():
|
|
176
|
+
self.route_http(pattern, response)
|
|
147
177
|
|
|
148
178
|
@property
|
|
149
179
|
def terminated(self) -> bool:
|
|
@@ -159,6 +189,32 @@ class Environment:
|
|
|
159
189
|
metadata, stdout, stderr = self._native.run(source, _as_bytes("stdin", stdin))
|
|
160
190
|
return _decode_result(metadata, stdout, stderr)
|
|
161
191
|
|
|
192
|
+
def run_python(
|
|
193
|
+
self,
|
|
194
|
+
source: str,
|
|
195
|
+
argv: Sequence[str] = (),
|
|
196
|
+
stdin: Union[bytes, bytearray, memoryview] = b"",
|
|
197
|
+
) -> RunResult:
|
|
198
|
+
"""Execute Python source directly in this environment.
|
|
199
|
+
|
|
200
|
+
The Python interpreter is fresh for each call. The simulated filesystem, cwd, resource
|
|
201
|
+
usage, and terminal exhaustion state belong to the persistent environment.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
if not isinstance(source, str):
|
|
205
|
+
raise TypeError("source must be str")
|
|
206
|
+
if isinstance(argv, (str, bytes, bytearray, memoryview)) or not isinstance(argv, Sequence):
|
|
207
|
+
raise TypeError("argv must be a sequence of str")
|
|
208
|
+
arguments = list(argv)
|
|
209
|
+
if not all(isinstance(argument, str) for argument in arguments):
|
|
210
|
+
raise TypeError("argv must be a sequence of str")
|
|
211
|
+
metadata, stdout, stderr = self._native.run_python(
|
|
212
|
+
source,
|
|
213
|
+
arguments,
|
|
214
|
+
_as_bytes("stdin", stdin),
|
|
215
|
+
)
|
|
216
|
+
return _decode_result(metadata, stdout, stderr)
|
|
217
|
+
|
|
162
218
|
def write_file(
|
|
163
219
|
self,
|
|
164
220
|
path: str,
|
|
@@ -196,6 +252,34 @@ class Environment:
|
|
|
196
252
|
raise TypeError("parents must be bool")
|
|
197
253
|
self._native.mkdir(path, parents)
|
|
198
254
|
|
|
255
|
+
def route_http(
|
|
256
|
+
self,
|
|
257
|
+
pattern: str,
|
|
258
|
+
response: HttpResponse,
|
|
259
|
+
*,
|
|
260
|
+
method: Optional[str] = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Register a static HTTP response for an exact URL or ``*`` glob.
|
|
263
|
+
|
|
264
|
+
Requests are handled inside the simulator. This does not grant the environment DNS,
|
|
265
|
+
sockets, TLS, or access to the host network. When ``method`` is omitted, the route matches
|
|
266
|
+
every HTTP method.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
if not isinstance(pattern, str):
|
|
270
|
+
raise TypeError("pattern must be str")
|
|
271
|
+
if not isinstance(response, HttpResponse):
|
|
272
|
+
raise TypeError("response must be a shellsim.HttpResponse instance")
|
|
273
|
+
if method is not None and not isinstance(method, str):
|
|
274
|
+
raise TypeError("method must be str or None")
|
|
275
|
+
if isinstance(response.status, bool) or not isinstance(response.status, int):
|
|
276
|
+
raise TypeError("response status must be int")
|
|
277
|
+
if not 0 <= response.status <= 0xFFFF:
|
|
278
|
+
raise ValueError("response status must fit in an unsigned 16-bit integer")
|
|
279
|
+
headers = _http_headers(response.headers)
|
|
280
|
+
body = response.body.encode() if isinstance(response.body, str) else _as_bytes("response body", response.body)
|
|
281
|
+
self._native.route_http(pattern, method, response.status, headers, body)
|
|
282
|
+
|
|
199
283
|
def mount(self, host_root: Union[str, os.PathLike[str]], destination: str = "/work") -> MountResult:
|
|
200
284
|
"""Copy an explicitly trusted host directory into the bounded VFS.
|
|
201
285
|
|
|
@@ -225,6 +309,7 @@ def run(
|
|
|
225
309
|
memory: Optional[int] = None,
|
|
226
310
|
disk: Optional[int] = None,
|
|
227
311
|
output: Optional[int] = None,
|
|
312
|
+
http: Optional[Mapping[str, HttpResponse]] = None,
|
|
228
313
|
) -> RunResult:
|
|
229
314
|
"""Execute one action in a fresh environment."""
|
|
230
315
|
|
|
@@ -234,6 +319,7 @@ def run(
|
|
|
234
319
|
memory=memory,
|
|
235
320
|
disk=disk,
|
|
236
321
|
output=output,
|
|
322
|
+
http=http,
|
|
237
323
|
).run(source, stdin)
|
|
238
324
|
|
|
239
325
|
|
|
@@ -251,6 +337,24 @@ def _as_bytes(name: str, value: Any) -> bytes:
|
|
|
251
337
|
return bytes(value)
|
|
252
338
|
|
|
253
339
|
|
|
340
|
+
def _http_headers(value: Any) -> list[Tuple[str, str]]:
|
|
341
|
+
if isinstance(value, Mapping):
|
|
342
|
+
headers = list(value.items())
|
|
343
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray, memoryview)):
|
|
344
|
+
headers = list(value)
|
|
345
|
+
else:
|
|
346
|
+
raise TypeError("response headers must be a mapping or sequence of name/value pairs")
|
|
347
|
+
normalized = []
|
|
348
|
+
for header in headers:
|
|
349
|
+
if not isinstance(header, Sequence) or isinstance(header, (str, bytes)) or len(header) != 2:
|
|
350
|
+
raise TypeError("response headers must contain name/value pairs")
|
|
351
|
+
name, item = header
|
|
352
|
+
if not isinstance(name, str) or not isinstance(item, str):
|
|
353
|
+
raise TypeError("response header names and values must be str")
|
|
354
|
+
normalized.append((name, item))
|
|
355
|
+
return normalized
|
|
356
|
+
|
|
357
|
+
|
|
254
358
|
def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResult:
|
|
255
359
|
metadata: Mapping[str, Any] = json.loads(metadata_json)
|
|
256
360
|
outcome = metadata["outcome"]
|
|
@@ -270,6 +374,18 @@ def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResul
|
|
|
270
374
|
)
|
|
271
375
|
for item in metadata["invocations"]
|
|
272
376
|
)
|
|
377
|
+
network_requests = tuple(
|
|
378
|
+
HttpRequest(
|
|
379
|
+
method=item["method"],
|
|
380
|
+
url=item["url"],
|
|
381
|
+
headers=tuple(tuple(header) for header in item["headers"]),
|
|
382
|
+
dropped_headers=item["dropped_headers"],
|
|
383
|
+
body_bytes=item["body_bytes"],
|
|
384
|
+
matched=item["matched"],
|
|
385
|
+
response_status=item["response_status"],
|
|
386
|
+
)
|
|
387
|
+
for item in metadata["network_requests"]
|
|
388
|
+
)
|
|
273
389
|
return RunResult(
|
|
274
390
|
returncode=outcome["exit_status"],
|
|
275
391
|
stdout=stdout,
|
|
@@ -283,8 +399,10 @@ def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResul
|
|
|
283
399
|
dropped_unsupported=metadata["dropped_unsupported"],
|
|
284
400
|
commands=tuple(metadata["commands"]),
|
|
285
401
|
dropped_commands=metadata["dropped_commands"],
|
|
286
|
-
|
|
402
|
+
unsupported_commands=tuple(metadata["unsupported_commands"]),
|
|
287
403
|
partial_commands=tuple(metadata["partial_commands"]),
|
|
288
404
|
invocations=invocations,
|
|
289
405
|
dropped_invocations=metadata["dropped_invocations"],
|
|
406
|
+
network_requests=network_requests,
|
|
407
|
+
dropped_network_requests=metadata["dropped_network_requests"],
|
|
290
408
|
)
|