Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/play/controls-and-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Command lists join commands on one line, as in `sh`:

Output redirection with `>` empties its file before the command runs, so `sort notes.txt > notes.txt` leaves an empty file, just as on Linux. If the command fails, the file is put back. Wildcards skip hidden names unless the pattern starts with a dot: `*` ignores `.env`, while `.*` matches it.

Background jobs (`&`), subshells, command substitution, special parameters such as `$?`, and file-descriptor redirection such as `2>` are rejected with an explanation before anything runs. Quote or escape these characters to use them as data.
`$?` holds the exit status of the last command, as on Linux: `ls /missing; echo $?` prints `2`. The lab reports `0` for success, `1` for most failures (including `grep` without a match and `false`), `2` for shell syntax errors and `ls`, `grep`, or `sort` trouble, `126` for a script that exists but cannot run, and `127` for a command the lab does not provide. A blank line keeps the previous status, and each script starts at `0`.

Background jobs (`&`), subshells, command substitution, other special parameters such as `$1` or `$$`, and file-descriptor redirection such as `2>` are rejected with an explanation before anything runs. Quote or escape these characters to use them as data.

## Mission navigation

Expand Down
19 changes: 12 additions & 7 deletions docs/technical/diagrams/command-execution-pipeline.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ flowchart LR
INPUT["Player command line"] --> LINE{"At most 64 KiB?"}
LINE -- No --> E1["Reject<br/>history unchanged"]
LINE -- Yes --> HISTORY["Record in bounded<br/>attempt history"]
HISTORY --> LEX["Lex<br/>quotes · escapes · variables<br/>comments · pipes · redirection"]
HISTORY --> SPLIT{"Split ; && || lists<br/>every pipeline's syntax valid?"}
SPLIT -- No --> E4["Reject the whole line<br/>nothing runs · $? = 2"]
SPLIT -- Yes --> LEX["Lex one pipeline<br/>quotes · escapes · variables · $?<br/>comments · pipes · redirection"]
LEX --> PARSE["Parse<br/>pipeline stages and redirects"]
PARSE --> EXPAND["Expand<br/>virtual globs and arguments"]
EXPAND --> PREFLIGHT{"Composition and<br/>resource checks pass?"}
Expand All @@ -14,12 +16,15 @@ flowchart LR
STATE --> OUTPUT{"Output within limit?"}
OUTPUT -- No --> E3["Reject bounded output"]
OUTPUT -- Yes --> REDIRECT{"Virtual output<br/>redirection?"}
REDIRECT -- Yes --> WRITE["Preflight and write<br/>virtual destination"]
REDIRECT -- Yes --> WRITE["Write virtual destination<br/>&gt; empties it before the stage<br/>restored if the stage fails"]
REDIRECT -- No --> NEXT{"Another pipeline stage?"}
WRITE --> NEXT
NEXT -- Yes --> READ
NEXT -- No --> RESULT["Execution result<br/>output · practiced commands<br/>pipeline width · editor request"]
RESULT --> VALIDATE["Outcome evaluation<br/>text comparison + Environment.Observe"]
NEXT -- No --> RESULT["Pipeline result<br/>output · exit status for $?<br/>practiced commands"]
RESULT --> LIST{"Another pipeline<br/>the operators allow?"}
LIST -- "Yes: ; always · && after success · || after failure" --> LEX
LIST -- No --> FINAL["Execution result<br/>ordered output and errors<br/>pipeline width · editor request"]
FINAL --> VALIDATE["Outcome evaluation<br/>text comparison + Environment.Observe"]

VFS[("In-memory filesystem")]
ENV[("Virtual environment")]
Expand All @@ -41,8 +46,8 @@ flowchart LR
classDef invariant fill:#ede9fe,color:#4c1d95,stroke:#7c3aed,stroke-width:2px

class INPUT input
class HISTORY,LEX,PARSE,EXPAND,READ,DISPATCH,RESULT,VALIDATE stage
class HISTORY,LEX,PARSE,EXPAND,READ,DISPATCH,RESULT,FINAL,VALIDATE stage
class STATE,WRITE,VFS,ENV,PROC,ARC state
class LINE,PREFLIGHT,OUTPUT,REDIRECT,NEXT decision
class E1,E2,E3 error
class LINE,SPLIT,PREFLIGHT,OUTPUT,REDIRECT,NEXT,LIST decision
class E1,E2,E3,E4 error
class HOST invariant
27 changes: 15 additions & 12 deletions docs/technical/diagrams/command-execution-pipeline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/technical/sandbox-and-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Editable source: [`command-execution-pipeline.mmd`](diagrams/command-execution-p

1. **Bound input:** reject a command line over 64 KiB before adding it to the attempt's 100-entry history.
2. **Split lists:** separate unquoted `;`, `&&`, and `||` into pipelines and check every pipeline's syntax before the first runs. Each pipeline then passes through the remaining stages only when its turn comes, so expansion sees earlier `cd` and `export` effects.
3. **Lex:** reject unquoted shell syntax outside the teaching subset (background jobs, subshells, substitutions, special parameters, and file-descriptor redirection), then recognize words, quotes, escapes, comments, variables, pipes, and `<`, `>`, or `>>`. Interactive and script lines share these rules. Expansion reads only the sandbox environment.
3. **Lex:** reject unquoted shell syntax outside the teaching subset (background jobs, subshells, substitutions, special parameters, and file-descriptor redirection), then recognize words, quotes, escapes, comments, variables, `$?`, pipes, and `<`, `>`, or `>>`. Interactive and script lines share these rules. Expansion reads only the sandbox environment and the attempt's last exit status.
4. **Parse:** build pipeline stages and attach at most one input and output redirection to each stage.
5. **Expand:** resolve eligible globs against the virtual filesystem and enforce expanded token and argument budgets.
6. **Preflight compositions:** reject unsupported interactive-editor or script placement before an earlier pipeline stage can mutate state.
Expand All @@ -80,7 +80,7 @@ One `sandbox.Sandbox` owns:
| --- | --- | --- |
| Files and directories | Normalized absolute paths to typed entries with content, mode, and owner; the virtual `HOME` directory always exists | Attempt only |
| Working directory | Virtual absolute path | Attempt only; child scripts restore caller scope |
| Environment | String map initialized with virtual `HOME` and `USER` | Attempt only; exported child-script values restore on return |
| Environment | String map initialized with virtual `HOME` and `USER`, plus the last exit status read by `$?` | Attempt only; exported child-script values restore on return, and a script's status becomes its caller's `$?` |
| Processes | Mission-provided PID map and running flags | Attempt only; no host PID is visible or signalable |
| Archives | Logical archive metadata and payload entries | Attempt only; kept synchronized with virtual path mutations |
| History | Last 100 accepted command lines | Attempt only |
Expand Down
12 changes: 7 additions & 5 deletions internal/sandbox/command_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,17 @@ func splitCommandList(line string) ([]commandListItem, error) {
// returned error is the last pipeline's failure, if any.
func (s *Sandbox) executeLine(line string, context *executionContext, allowInteractive bool) (Result, error) {
items, err := splitCommandList(line)
if err == nil && len(items) > 1 {
err = s.preflightCommandList(items, allowInteractive)
}
if err != nil {
return Result{}, err
// Like sh, a syntax error runs nothing and sets $? to 2.
s.lastStatus = statusTrouble
return Result{}, withExitStatus(statusTrouble, err)
}
if len(items) == 1 {
return s.executePipeline(items[0].line, context, allowInteractive)
}
if err := s.preflightCommandList(items, allowInteractive); err != nil {
return Result{}, err
}

result := Result{list: true}
var output strings.Builder
Expand Down Expand Up @@ -179,7 +181,7 @@ func (s *Sandbox) preflightCommandList(items []commandListItem, allowInteractive
if err := validateShellSyntax(item.line); err != nil {
return err
}
tokens, err := lex(item.line, s.Env)
tokens, err := lex(item.line, s.Env, s.lastStatus)
if err != nil {
return err
}
Expand Down
Loading
Loading