Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

228 lines
12 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Bash
description: Run bash commands with `execute()`, per-call `cwd`/`env` overrides, and mid-flight cancellation.
icon: terminal
---
Mirage Bash is how agents act on the workspace. `execute()` parses a bash-style command, looks up the target session, resolves mounts, runs the executor, applies I/O side effects, and records history through the [Observer](/home/observer).
## Per-call overrides: `cwd`, `env`
Providing `cwd` or `env` runs the command in an ephemeral session clone, like a bash subshell `(cd /data && cmd)`. Mutations like `cd` or `export` inside the call do NOT persist back to the workspace's session. To change persistent state, run the command without these options.
<Tabs>
<Tab title="Python" icon="/images/python-logo.svg">
```python
# Persistent mutation (no options): like `cd /data; cmd`
await ws.execute("cd /data")
await ws.execute("ls") # sees /data
# One-shot subshell (with cwd): like `(cd /data && cmd)`
await ws.execute("ls", cwd="/data")
# ws.cwd is unchanged; mutations inside don't leak
```
</Tab>
<Tab title="TypeScript" icon="/images/typescript-logo.svg">
```typescript
// Persistent mutation
await ws.execute("cd /data")
await ws.execute("ls") // sees /data
// One-shot subshell
await ws.execute("ls", { cwd: "/data" })
// ws.cwd is unchanged; mutations inside don't leak
```
</Tab>
<Tab title="CLI" icon="terminal">
```bash
# Use a real bash subshell inside the command string
mirage execute -w demo -c "(cd /data && ls)"
mirage execute -w demo -c "(export FOO=bar; printenv FOO)"
```
The CLI doesn't have `--cwd` / `--env` flags, but bash subshell syntax `(cd ... && cmd)` and `(export FOO=bar; cmd)` give the same per-call isolation. Mutations inside the parens don't leak.
</Tab>
</Tabs>
This makes per-call overrides safe under concurrent calls on the same session. Two parallel `execute()` calls with different `cwd` see their own cwd without cross-contamination, even on the same session.
## Subshells `(...)`
Wrapping commands in `( ... )` runs them in an isolated copy of the session: `cd`, `export`, and other mutations inside the parens do not leak back. It is the same isolation as the `cwd` / `env` overrides above, and the CLI's stand-in for them (there are no `--cwd` / `--env` flags).
```bash
(cd /data && ls) # cwd change scoped to the subshell
(export TOKEN=xyz; printenv) # env var gone once the parens close
```
The isolation holds under concurrency, and subshells are still covered by the per-session mount allowlist and the cancellation boundaries below.
## Mid-flight cancellation: `cancel` / `signal`
Both bindings support cooperative cancellation observed at recursion boundaries (LIST, PIPELINE, FOR/WHILE/UNTIL iterations, COMMAND, subshells, command substitution) and inside `sleep`. On cancel, the call raises an abort error.
<Tabs>
<Tab title="Python" icon="/images/python-logo.svg">
```python
import asyncio
from mirage.workspace.abort import MirageAbortError
cancel = asyncio.Event()
async def trigger():
await asyncio.sleep(0.1)
cancel.set()
asyncio.create_task(trigger())
try:
await ws.execute("sleep 5", cancel=cancel)
except MirageAbortError:
print("aborted")
```
</Tab>
<Tab title="TypeScript" icon="/images/typescript-logo.svg">
```typescript
try {
await ws.execute("sleep 5", { signal: AbortSignal.timeout(100) })
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") {
console.log("aborted")
}
}
```
</Tab>
<Tab title="CLI" icon="terminal">
```bash
# Background the job, then cancel it
JOB_ID=$(mirage execute -w demo -c "sleep 60" --bg)
mirage job cancel "$JOB_ID"
```
Per-call timeout is not a CLI flag yet. Use `--bg` to get a job id and `mirage job cancel` to terminate, or wrap the command in the `timeout` builtin: `mirage execute -w demo -c "timeout 30 <cmd>"` exits `124` on overrun.
</Tab>
</Tabs>
## Three Scopes for State
| Need | API | Bash equivalent |
| --- | --- | --- |
| One isolated command | `execute(cmd, cwd=..., env=...)` | `(cd /data && cmd)` |
| Many isolated commands sharing scoped state | `session_id=...` (Py) / `sessionId` (TS) | a separate terminal |
| Persistent shell mutations | run without options | `cd /data; cmd` |
## Supported bash syntax
Mirage Bash is a tree-sitter-bash parser plus a custom executor. It implements the constructs LLMs reach for most often. What is not supported returns a clear, parseable error so an agent can self-correct on its next turn.
### Supported
- **Operators:** pipes `|`, `|&`; lists `&&`, `||`, `;`; background `&`.
- **Redirects:** `>`, `>>`, `<`, `2>`, `2>&1`, `&>`, `&>>`, heredoc `<<`, herestring `<<<`.
- **Substitutions:** command substitution `` `cmd` `` and `$(cmd)`; arithmetic `$((expr))`; parameter expansion `${VAR}`, `${VAR:-default}`, `${VAR%suffix}`, etc.; input-direction process substitution `<(cmd)`.
- **Control flow:** `if`/`elif`/`else`/`fi`, `for`, `while`, `until`, `case`, `select`, `function name() {}`, `break`, `continue`, `return`.
- **Grouping:** subshells `(cmd)`, compound `{ cmd; }`, negation `! cmd`.
- **Builtins:** `cd`, `pwd`, `echo`, `printf`, `printenv`, `read`, `source`, `.`, `eval`, `export`, `unset`, `local`, `set`, `shift`, `trap` (no-op), `test`, `[`, `[[`, `true`, `false`, `sleep`, `xargs`, `timeout`, `bash`, `sh`, `python`, `python3`.
- **Builtin options (GNU semantics):** `echo -n/-e/-E` (leading-word option rule: `echo hi -n` prints `hi -n`), `read -r`, `xargs -n/-0/-d/-r/--` (batching, GNU exit codes: `123` when an invocation fails, `126`/`127` stop the run), `timeout DURATION` with `s`/`m`/`h`/`d` suffixes (kills at the deadline with exit `124`, usage errors exit `125`). `shift` and `return` report bash's `numeric argument required` errors.
- **Globs:** `*`, `?`, `[...]` classes and `[!...]` negation (Python `fnmatch` semantics in both implementations), resolved by the shell or pushed down to the resource.
- **Comments:** `#`.
### Unsupported (returns clear error)
- **Job control:** `bg`, `disown`. (`fg`, `jobs`, `wait`, `kill`, `ps` work; use the `--background` flag and `mirage job` CLI for long-running work.)
- **Shell internals:** `exec`, `complete`, `compgen`, `ulimit`.
- **Output process substitution:** `>(cmd)` (the `<(cmd)` direction works).
- **Builtin options with no process backing:** `xargs -I`/`-P` (exit `1`) and `timeout -s`/`-k`/`--preserve-status` (exit `125`) return an `unsupported option` error: commands run as coroutines inside the workspace, so there is no process to signal or parallelize.
Each returns `exit_code 2` with stderr `mirage: unsupported builtin: <name>` or `mirage: unsupported: process substitution >(...)`, except the builtin options above, which use the listed GNU-shaped exit codes.
### Syntax errors
Commands the parser cannot make sense of return `exit_code 2` with stderr `mirage: syntax error near '<token>'`. Earlier versions silently ran whatever fragment did parse; that no longer happens.
### What `--background` is and isn't
The daemon's `--background` flag detaches a job and returns a job id. It is not the same as the bash `&` operator, which the shell does support inline (`sleep 30 &`). Use `&` for in-shell job parallelism, `--background` (or `mirage job`) for long-lived work that should outlive the request.
## Per-session mount capability
A session can be created with an explicit allowlist of mount prefixes. Any command whose path resolves to a mount outside that list is rejected with `mirage: session 'agent' not allowed to access mount '/X'` and exit code 1. Default sessions (no allowlist) keep their current unrestricted behavior, so existing code is unaffected.
This is a soft boundary, enforced inside the daemon process, not an OS or process-level isolation. Use it to shrink the blast radius of prompt-injection in multi-agent workspaces: a Slack-only agent cannot pivot to read `/linear`, `/github`, or any other mount it was not given.
The check fires for every code path that reaches a mount: shell commands (`cat`, `ls`, ...), redirects (`>`, `<`), cross-mount `cp`/`mv`, `wget -O`, `curl -o`, command substitution `$(...)`, subshells `(...)`, pipes, `&&`/`||` chains, background jobs, and the programmatic `ws.ops.read/write/...` API. Two infrastructure prefixes are always allowed regardless of the allowlist: the history view (`/.bash_history`, which the `history` builtin and the GNU histfile render from) and the virtual root (`/`, where stateless text-processing commands like `wc` resolve when given no path).
<Tabs>
<Tab title="Python" icon="/images/python-logo.svg">
```python
ws = Workspace({
"/s3": s3,
"/slack": slack,
"/linear": linear,
})
ws.create_session("slack-agent", allowed_mounts={"/slack"})
ws.create_session("data-agent", allowed_mounts={"/s3"})
await ws.execute("ls /slack", session_id="slack-agent") # ok
await ws.execute("cat /linear/issues/SEC-42",
session_id="slack-agent")
# exit_code=1, stderr=b"session 'slack-agent' not allowed to "
# b"access mount '/linear'\n"
```
</Tab>
<Tab title="CLI" icon="terminal">
```bash
# Repeat --mount (or -m) per allowed prefix
mirage session create demo --id slack-agent --mount /slack
mirage session create demo --id data-agent -m /s3 -m /github
mirage execute -w demo -s slack-agent -c "cat /linear/issues/SEC-42"
# mirage: session 'slack-agent' not allowed to access mount '/linear'
```
</Tab>
</Tabs>
The allowlist is a property of the session, so it covers every command issued under that `session_id`, including subshells, pipelines, and recursive `bash -c '...'`. It does not change `MountMode`: a write to a mount in the allowlist is still rejected if the mount is `READ`. The two checks compose.
## Agent Pattern
Agent harnesses commonly fan out tool calls in parallel, each with its own `cwd`/`env`/`cancel`. The clone semantics make this race-free without per-call boilerplate. From the CLI, a subshell per call gives the same isolation.
<Tabs>
<Tab title="Python" icon="/images/python-logo.svg">
```python
async def tool_call(cmd: str, cwd: str, env: dict[str, str], timeout: float):
cancel = asyncio.Event()
asyncio.get_event_loop().call_later(timeout, cancel.set)
return await ws.execute(cmd, cwd=cwd, env=env, cancel=cancel)
results = await asyncio.gather(
tool_call("ls", "/data", {"DEBUG": "1"}, 5.0),
tool_call("grep foo *.log", "/logs", {"DEBUG": "1"}, 5.0),
)
```
</Tab>
<Tab title="TypeScript" icon="/images/typescript-logo.svg">
```typescript
async function toolCall(
cmd: string,
cwd: string,
env: Record<string, string>,
timeoutMs: number,
) {
return ws.execute(cmd, { cwd, env, signal: AbortSignal.timeout(timeoutMs) })
}
const results = await Promise.all([
toolCall("ls", "/data", { DEBUG: "1" }, 5000),
toolCall("grep foo *.log", "/logs", { DEBUG: "1" }, 5000),
])
```
</Tab>
<Tab title="CLI" icon="terminal">
```bash
# No --cwd/--env flags: isolate each parallel call in a subshell
mirage execute -w demo -c "(cd /data && export DEBUG=1 && ls) & (cd /logs && export DEBUG=1 && grep foo *.log) & wait"
```
Each `( ... )` runs in its own scope, so the parallel `cd` / `export` don't collide. `&` backgrounds them inside Mirage Bash and `wait` joins.
</Tab>
</Tabs>