Skip to content

Commit f774f1f

Browse files
samuelcolvinclaude
andauthored
Pseudo filesystem support (#85)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9b8aa64 commit f774f1f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+7892
-222
lines changed

.claude/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"Bash(git mv:*)",
2828
"Bash(git log:*)",
2929
"Bash(git grep:*)",
30-
"Bash(git commit:*)",
3130
"Bash(gh pr view:*)",
3231
"Bash(gh api:*)",
3332
"Bash(gh run view:*)",
@@ -43,7 +42,8 @@
4342
"WebFetch(domain:docs.anthropic.com)",
4443
"WebFetch(domain:github.com)",
4544
"WebFetch(domain:15r10nk.github.io)",
46-
"WebFetch(domain:docs.rs)"
45+
"WebFetch(domain:docs.rs)",
46+
"WebFetch(domain:pyo3.rs)"
4747
],
4848
"deny": [],
4949
"ask": []

CLAUDE.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ Project goals:
1414
- **Snapshotting and iteration**: Plan is to allow code to be iteratively executed and snapshotted at each function call
1515
- Targets the latest stable version of Python, currently Python 3.14
1616

17+
## Important Security Notice
18+
19+
It's ABSOLUTELY CRITICAL that there's no way for code run in a Monty sandbox to access the host filesystem, or environment or to in any way "escape the sandbox".
20+
21+
**Monty will be used to run untrusted, potentially malicious code.**
22+
23+
Make sure there's no risk of this, either in the implementation, or in the public API that makes it more like that a developer using the pydantic_monty package might make such a mistake.
24+
25+
Possible security risks to consider:
26+
* filesystem access
27+
* path traversal to access files the users did not intend to expose to the monty sandbox
28+
* memory errors - use of unsafe memory operations
29+
* excessive memory usage - evading monty's resource limits
30+
* infinite loops - evading monty's resource limits
31+
* network access - sockets, HTTP requests
32+
* subprocess/shell execution - os.system, subprocess, etc.
33+
* import system abuse - importing modules with side effects or accessing `__import__`
34+
* external function/callback misuse - callbacks run in host environment
35+
* deserialization attacks - loading untrusted serialized Monty/snapshot data
36+
* regex/string DoS - catastrophic backtracking or operations bypassing limits
37+
* information leakage via timing or error messages
38+
* Python/Javascript/Rust APIs that accidentally allow developers to expose their host to monty code
39+
1740
## Bytecode VM Architecture
1841

1942
Monty is implemented as a bytecode VM, same as CPython.
@@ -118,13 +141,17 @@ explain what it does and why and any considerations or potential foot-guns of us
118141

119142
The only exception is trait implementation methods where a docstring is not necessary if the method is self-explanatory.
120143

144+
It's important that docstrings cover the motivation and primary usage patterns of code, not just the simple "what it does".
145+
146+
Similarly, you should add comments to code, especially if the code is complex or esoteric.
147+
121148
Only add examples to docstrings of public functions and structs, examples should be <=8 lines, if the example is more, remove it.
122149

123150
If you add example code to docstrings, it must be run in tests. NEVER add examples that are ignored.
124151

125-
Similarly, you should add lots of comments to code.
152+
If you encounter a comment or docstring that's out of date - you MUST update it to be correct.
126153

127-
If you see a comment or docstring that's out of date - you MUST update it to be correct.
154+
Similarly, if you encounter code that has no docstrings or comments, or they are minimal, you should add more detail.
128155

129156
NOTE: COMMENTS AND DOCSTRINGS ARE EXTREMELY IMPORTANT TO THE LONG TERM HEALTH OF THE PROJECT.
130157

@@ -329,6 +356,9 @@ Use `@pytest.mark.parametrize` whenever testing multiple similar cases.
329356

330357
Use `snapshot` from `inline-snapshot` for all test asserts.
331358

359+
NEVER do the lazy `assert '...' in ...` instead always do `assert value == snapshot()`,
360+
then run the test and inline-snapshot will fill in the missing value in the `snapshot()` call.
361+
332362
Use `pytest.raises` for expected exceptions, like this
333363

334364
```py

crates/monty-cli/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ fn main() -> ExitCode {
101101
eprintln!("{elapsed:?}, async futures not supported in CLI: {pending:?}");
102102
return ExitCode::FAILURE;
103103
}
104+
RunProgress::OsCall { function, args, .. } => {
105+
let elapsed = start.elapsed();
106+
eprintln!("{elapsed:?}, OS calls not supported in CLI: {function:?}({args:?})");
107+
return ExitCode::FAILURE;
108+
}
104109
}
105110
}
106111
} else {

crates/monty-js/src/convert.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ pub fn monty_to_js<'e>(obj: &MontyObject, env: &'e Env) -> Result<JsMontyObject<
8080
MontyObject::Bytes(bytes) => create_js_buffer(bytes, env)?,
8181
MontyObject::List(items) => create_js_array(items, env)?.into_unknown(env)?,
8282
MontyObject::Tuple(items) => create_js_tuple(items, env)?,
83+
// NamedTuple is converted to a tuple (loses named access in JS)
84+
MontyObject::NamedTuple { values, .. } => create_js_tuple(values, env)?,
8385
MontyObject::Dict(pairs) => create_js_map(pairs, env)?,
8486
MontyObject::Set(items) | MontyObject::FrozenSet(items) => create_js_set(items, env)?,
8587
MontyObject::Exception { exc_type, arg } => create_js_exception(*exc_type, arg.as_deref(), env)?,
@@ -93,6 +95,7 @@ pub fn monty_to_js<'e>(obj: &MontyObject, env: &'e Env) -> Result<JsMontyObject<
9395
methods,
9496
frozen,
9597
} => create_js_dataclass(name, *type_id, field_names, attrs, methods, *frozen, env)?,
98+
MontyObject::Path(p) => env.create_string(p)?.into_unknown(env)?,
9699
MontyObject::Repr(s) | MontyObject::Cycle(_, s) => env.create_string(s)?.into_unknown(env)?,
97100
};
98101
Ok(JsMontyObject(unknown))

crates/monty-js/src/monty_cls.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ impl Monty {
270270
"Async futures are not supported in synchronous run(). Use start() for async execution.",
271271
));
272272
}
273+
RunProgress::OsCall { function, .. } => {
274+
return Err(Error::from_reason(format!(
275+
"OS calls are not supported: {function:?}",
276+
)));
277+
}
273278
}
274279
}
275280
}};
@@ -720,6 +725,9 @@ where
720725
RunProgress::ResolveFutures(_) => {
721726
panic!("Async futures (ResolveFutures) are not yet supported in the JS bindings")
722727
}
728+
RunProgress::OsCall { function, .. } => {
729+
panic!("OS calls are not yet supported in the JS bindings: {function:?}")
730+
}
723731
}
724732
}
725733

crates/monty-python/python/pydantic_monty/__init__.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@
1818
MontyTypingError,
1919
__version__,
2020
)
21+
from .os_access import AbstractFile, AbstractOS, CallbackFile, MemoryFile, OSAccess, OsFunction, StatResult
2122

2223
__all__ = (
24+
# this file
25+
'run_monty_async',
26+
'ExternalResult',
27+
'ResourceLimits',
28+
# _monty
29+
'__version__',
2330
'Monty',
2431
'MontyComplete',
2532
'MontySnapshot',
@@ -29,10 +36,14 @@
2936
'MontyRuntimeError',
3037
'MontyTypingError',
3138
'Frame',
32-
'__version__',
33-
'run_monty_async',
34-
'ResourceLimits',
35-
'ExternalResult',
39+
# os_access
40+
'StatResult',
41+
'OsFunction',
42+
'AbstractOS',
43+
'AbstractFile',
44+
'MemoryFile',
45+
'CallbackFile',
46+
'OSAccess',
3647
)
3748
T = TypeVar('T')
3849

crates/monty-python/python/pydantic_monty/_monty.pyi

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ from typing import Any, Callable, Literal, final, overload
44
from typing_extensions import Self
55

66
from . import ExternalResult, ResourceLimits
7+
from .os_access import OsFunction
78

89
__all__ = [
10+
'__version__',
911
'Monty',
1012
'MontyComplete',
1113
'MontySnapshot',
@@ -15,7 +17,6 @@ __all__ = [
1517
'MontyRuntimeError',
1618
'MontyTypingError',
1719
'Frame',
18-
'__version__',
1920
]
2021
__version__: str
2122

@@ -83,17 +84,22 @@ class Monty:
8384
limits: ResourceLimits | None = None,
8485
external_functions: dict[str, Callable[..., Any]] | None = None,
8586
print_callback: Callable[[Literal['stdout'], str], None] | None = None,
87+
os: Callable[[OsFunction, tuple[Any, ...]], Any] | None = None,
8688
) -> Any:
8789
"""
8890
Execute the code and return the result.
8991
90-
The GIL is released allowing parallel execution if `print_callback` is `None`.
92+
The GIL is released allowing parallel execution.
9193
9294
Arguments:
9395
inputs: Dict of input variable values (must match names from __init__)
9496
limits: Optional resource limits configuration
9597
external_functions: Dict of external function callbacks (must match names from __init__)
9698
print_callback: Optional callback for print output
99+
os: Optional callback for OS calls.
100+
Called with (function_name, args) where function_name is like 'Path.exists'
101+
and args is a tuple of arguments. Must return the appropriate value for the
102+
OS function (e.g., bool for exists(), stat_result for stat()).
97103
98104
Returns:
99105
The result of the last expression in the code
@@ -114,7 +120,7 @@ class Monty:
114120
115121
This allows you to iteratively run code and parse/resume whenever an external function is called.
116122
117-
The GIL is released allowing parallel execution if `print_callback` is `None`.
123+
The GIL is released allowing parallel execution.
118124
119125
Arguments:
120126
inputs: Dict of input variable values (must match names from __init__)
@@ -196,8 +202,15 @@ class MontySnapshot:
196202
"""The name of the script being executed."""
197203

198204
@property
199-
def function_name(self) -> str:
200-
"""The name of the external function being called."""
205+
def is_os_function(self) -> bool:
206+
"""Whether this snapshot is for an OS function call (e.g., Path.stat)."""
207+
208+
@property
209+
def function_name(self) -> str | OsFunction:
210+
"""The name of the function being called (external function or OS function like 'Path.stat').
211+
212+
Will be a `OsFunction` if `is_os_function` is `True`.
213+
"""
201214

202215
@property
203216
def args(self) -> tuple[Any, ...]:
@@ -217,7 +230,7 @@ class MontySnapshot:
217230
218231
`resume` may only be called once on each MontySnapshot instance.
219232
220-
The GIL is released allowing parallel execution if `print_callback` is `None`.
233+
The GIL is released allowing parallel execution.
221234
222235
Arguments:
223236
return_value: The value to return from the external function call.
@@ -325,7 +338,7 @@ class MontyFutureSnapshot:
325338
326339
`resume` may only be called once on each MontyFutureSnapshot instance.
327340
328-
The GIL is released allowing parallel execution if `print_callback` is `None`.
341+
The GIL is released allowing parallel execution.
329342
330343
Arguments:
331344
results: Dict mapping call_id to result dict. Each result dict must have

0 commit comments

Comments
 (0)