Skip to content

Commit cc22d17

Browse files
authored
refactor(bindings): unify mount API, read-only by default (#1154)
Make mounts read-only by default across JS and Python bindings. Align both to the same conceptual mount API: files dict + mounts list with writable opt-in. Add binding parity spec and maintenance check.
1 parent 19437c0 commit cc22d17

File tree

9 files changed

+187
-204
lines changed

9 files changed

+187
-204
lines changed

crates/bashkit-js/__test__/runtime-compat/vfs.test.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ describe("VFS API", () => {
122122
assert.ok(entries.some((e) => e.name === "fsapi.txt"));
123123
});
124124

125-
it("mountReal and unmount", () => {
125+
it("mount and unmount", () => {
126126
const bash = new Bash();
127-
// Mount /tmp as a real filesystem at /host-tmp
128-
bash.mountReal("/tmp", "/host-tmp", true);
127+
// Mount /tmp as a real filesystem at /host-tmp (read-only by default)
128+
bash.mount("/tmp", "/host-tmp");
129129
// The mount should be accessible
130130
const r = bash.executeSync("ls /host-tmp 2>/dev/null; echo status=$?");
131131
assert.ok(r.stdout.includes("status=0"));

crates/bashkit-js/src/lib.rs

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ pub struct MountConfig {
411411
pub host_path: String,
412412
/// VFS path where mount appears (defaults to host_path).
413413
pub vfs_path: Option<String>,
414-
/// If true, mount is read-only (default: true).
415-
pub read_only: Option<bool>,
414+
/// If true, mount is read-write (default: false → read-only).
415+
pub writable: Option<bool>,
416416
}
417417

418418
/// Options for creating a Bash or BashTool instance.
@@ -444,7 +444,7 @@ pub struct BashOptions {
444444
/// Files to mount in the virtual filesystem.
445445
/// Keys are absolute paths, values are file content strings.
446446
pub files: Option<HashMap<String, String>>,
447-
/// Real filesystem mounts. Each entry: { hostPath, vfsPath?, readOnly? }
447+
/// Real filesystem mounts. Each entry: { hostPath, vfsPath?, writable? }
448448
pub mounts: Option<Vec<MountConfig>>,
449449
/// Enable embedded Python execution (`python`/`python3` builtins).
450450
pub python: Option<bool>,
@@ -849,21 +849,20 @@ impl Bash {
849849

850850
/// Mount a host directory into the VFS at runtime.
851851
///
852-
/// `readOnly` defaults to true when omitted.
852+
/// Read-only by default; pass `writable: true` to enable writes.
853853
#[napi]
854-
pub fn mount_real(
854+
pub fn mount(
855855
&self,
856856
host_path: String,
857857
vfs_path: String,
858-
read_only: Option<bool>,
858+
writable: Option<bool>,
859859
) -> napi::Result<()> {
860860
block_on_with(&self.state, |s| async move {
861861
let bash = s.inner.lock().await;
862-
let ro = read_only.unwrap_or(true);
863-
let mode = if ro {
864-
bashkit::RealFsMode::ReadOnly
865-
} else {
862+
let mode = if writable.unwrap_or(false) {
866863
bashkit::RealFsMode::ReadWrite
864+
} else {
865+
bashkit::RealFsMode::ReadOnly
867866
};
868867
let real_backend = bashkit::RealFs::new(&host_path, mode)
869868
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
@@ -1222,21 +1221,20 @@ impl BashTool {
12221221

12231222
/// Mount a host directory into the VFS at runtime.
12241223
///
1225-
/// `readOnly` defaults to true when omitted.
1224+
/// Read-only by default; pass `writable: true` to enable writes.
12261225
#[napi]
1227-
pub fn mount_real(
1226+
pub fn mount(
12281227
&self,
12291228
host_path: String,
12301229
vfs_path: String,
1231-
read_only: Option<bool>,
1230+
writable: Option<bool>,
12321231
) -> napi::Result<()> {
12331232
block_on_with(&self.state, |s| async move {
12341233
let bash = s.inner.lock().await;
1235-
let ro = read_only.unwrap_or(true);
1236-
let mode = if ro {
1237-
bashkit::RealFsMode::ReadOnly
1238-
} else {
1234+
let mode = if writable.unwrap_or(false) {
12391235
bashkit::RealFsMode::ReadWrite
1236+
} else {
1237+
bashkit::RealFsMode::ReadOnly
12401238
};
12411239
let real_backend = bashkit::RealFs::new(&host_path, mode)
12421240
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
@@ -1621,12 +1619,12 @@ fn build_bash_from_state(state: &SharedState, files: Option<&HashMap<String, Str
16211619
// Apply real filesystem mounts
16221620
if let Some(ref mounts) = state.mounts {
16231621
for m in mounts {
1624-
let read_only = m.read_only.unwrap_or(true);
1625-
builder = match (read_only, &m.vfs_path) {
1626-
(true, None) => builder.mount_real_readonly(&m.host_path),
1627-
(true, Some(vfs)) => builder.mount_real_readonly_at(&m.host_path, vfs),
1628-
(false, None) => builder.mount_real_readwrite(&m.host_path),
1629-
(false, Some(vfs)) => builder.mount_real_readwrite_at(&m.host_path, vfs),
1622+
let writable = m.writable.unwrap_or(false);
1623+
builder = match (writable, &m.vfs_path) {
1624+
(false, None) => builder.mount_real_readonly(&m.host_path),
1625+
(false, Some(vfs)) => builder.mount_real_readonly_at(&m.host_path, vfs),
1626+
(true, None) => builder.mount_real_readwrite(&m.host_path),
1627+
(true, Some(vfs)) => builder.mount_real_readwrite_at(&m.host_path, vfs),
16301628
};
16311629
}
16321630
}

crates/bashkit-js/wrapper.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ export interface BashOptions {
7070
* ```typescript
7171
* const bash = new Bash({
7272
* mounts: [
73-
* { path: "/docs", root: "/real/path/to/docs", readOnly: true },
73+
* { path: "/docs", root: "/real/path/to/docs" },
7474
* ],
7575
* });
7676
* ```
7777
*/
78-
mounts?: Array<{ path: string; root: string; readOnly?: boolean }>;
78+
mounts?: Array<{ path: string; root: string; writable?: boolean }>;
7979
/**
8080
* Enable embedded Python execution (`python`/`python3` builtins).
8181
*
@@ -152,7 +152,7 @@ function toNativeOptions(
152152
mounts: options?.mounts?.map((m) => ({
153153
hostPath: m.root,
154154
vfsPath: m.path,
155-
readOnly: m.readOnly,
155+
writable: m.writable,
156156
})),
157157
python: options?.python,
158158
externalFunctions: options?.externalFunctions,
@@ -421,9 +421,9 @@ export class Bash {
421421
return this.native.fs();
422422
}
423423

424-
/** Mount a host directory into the VFS. readOnly defaults to true. */
425-
mountReal(hostPath: string, vfsPath: string, readOnly?: boolean): void {
426-
this.native.mountReal(hostPath, vfsPath, readOnly);
424+
/** Mount a host directory into the VFS. Read-only by default; pass writable: true to enable writes. */
425+
mount(hostPath: string, vfsPath: string, writable?: boolean): void {
426+
this.native.mount(hostPath, vfsPath, writable);
427427
}
428428

429429
/** Unmount a previously mounted filesystem. */
@@ -653,9 +653,9 @@ export class BashTool {
653653
return this.native.fs();
654654
}
655655

656-
/** Mount a host directory into the VFS. readOnly defaults to true. */
657-
mountReal(hostPath: string, vfsPath: string, readOnly?: boolean): void {
658-
this.native.mountReal(hostPath, vfsPath, readOnly);
656+
/** Mount a host directory into the VFS. Read-only by default; pass writable: true to enable writes. */
657+
mount(hostPath: string, vfsPath: string, writable?: boolean): void {
658+
this.native.mount(hostPath, vfsPath, writable);
659659
}
660660

661661
/** Unmount a previously mounted filesystem. */

crates/bashkit-python/README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,28 +99,34 @@ fs.symlink("/data/link", "/data/blob.bin")
9999
fs.chmod("/data/blob.bin", 0o644)
100100
```
101101

102+
### Files and Mounts
103+
104+
```python
105+
from bashkit import Bash, FileSystem
106+
107+
# Text files (in-memory, writable)
108+
bash = Bash(files={"/config/app.conf": "debug=true\n"})
109+
110+
# Real filesystem mounts (read-only by default)
111+
bash = Bash(mounts=[
112+
{"host_path": "/path/to/data", "vfs_path": "/data"},
113+
{"host_path": "/path/to/workspace", "vfs_path": "/workspace", "writable": True},
114+
])
115+
```
116+
102117
### Live Mounts
103118

104119
```python
105120
from bashkit import Bash, FileSystem
106121

107122
bash = Bash()
108-
workspace = FileSystem.real("/path/to/workspace", readwrite=True)
123+
workspace = FileSystem.real("/path/to/workspace", writable=True)
109124
bash.mount("/workspace", workspace)
110125

111126
bash.execute_sync("echo 'hello' > /workspace/demo.txt")
112127
bash.unmount("/workspace")
113128
```
114129

115-
`Bash` and `BashTool` also still support constructor-time mounts:
116-
117-
- `mount_text=[("/path", "content")]`
118-
- `mount_readonly_text=[("/path", "content")]`
119-
- `mount_real_readonly=["/host/path"]`
120-
- `mount_real_readonly_at=[("/host/path", "/vfs/path")]`
121-
- `mount_real_readwrite=["/host/path"]`
122-
- `mount_real_readwrite_at=[("/host/path", "/vfs/path")]`
123-
124130
### BashTool — Convenience Wrapper for AI Agents
125131

126132
`BashTool` is a convenience wrapper specifically designed for AI agents. It wraps `Bash` and adds contract metadata (`description`, Markdown `help`, `system_prompt`, JSON schemas) needed by tool-use protocols. Use this when integrating with LangChain, PydanticAI, or similar agent frameworks.

crates/bashkit-python/bashkit/_bashkit.pyi

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class FileSystem:
88

99
def __init__(self) -> None: ...
1010
@staticmethod
11-
def real(host_path: str, readwrite: bool = False) -> FileSystem: ...
11+
def real(host_path: str, writable: bool = False) -> FileSystem: ...
1212
def read_file(self, path: str) -> bytes: ...
1313
def write_file(self, path: str, content: bytes) -> None: ...
1414
def append_file(self, path: str, content: bytes) -> None: ...
@@ -61,15 +61,12 @@ class Bash:
6161
hostname: str | None = None,
6262
max_commands: int | None = None,
6363
max_loop_iterations: int | None = None,
64+
max_memory: int | None = None,
6465
python: bool = False,
6566
external_functions: list[str] | None = None,
6667
external_handler: ExternalHandler | None = None,
67-
mount_text: list[tuple[str, str]] | None = None,
68-
mount_readonly_text: list[tuple[str, str]] | None = None,
69-
mount_real_readonly: list[str] | None = None,
70-
mount_real_readonly_at: list[tuple[str, str]] | None = None,
71-
mount_real_readwrite: list[str] | None = None,
72-
mount_real_readwrite_at: list[tuple[str, str]] | None = None,
68+
files: dict[str, str] | None = None,
69+
mounts: list[dict[str, Any]] | None = None,
7370
) -> None: ...
7471
async def execute(self, commands: str) -> ExecResult: ...
7572
def execute_sync(self, commands: str) -> ExecResult: ...
@@ -122,12 +119,9 @@ class BashTool:
122119
hostname: str | None = None,
123120
max_commands: int | None = None,
124121
max_loop_iterations: int | None = None,
125-
mount_text: list[tuple[str, str]] | None = None,
126-
mount_readonly_text: list[tuple[str, str]] | None = None,
127-
mount_real_readonly: list[str] | None = None,
128-
mount_real_readonly_at: list[tuple[str, str]] | None = None,
129-
mount_real_readwrite: list[str] | None = None,
130-
mount_real_readwrite_at: list[tuple[str, str]] | None = None,
122+
max_memory: int | None = None,
123+
files: dict[str, str] | None = None,
124+
mounts: list[dict[str, Any]] | None = None,
131125
) -> None: ...
132126
async def execute(self, commands: str) -> ExecResult: ...
133127
def execute_sync(self, commands: str) -> ExecResult: ...

0 commit comments

Comments
 (0)