Examples
Everything below comes from the examples/ folder of the repository. The permission trees and the errors are the real output of sub explain and sub check, captured when this page was built.
Scripts that check
Section titled “Scripts that check”capabilities.sub
Hand a helper less than the script has: only the guides folder, not all of ./docs.
// Hand a helper less than the script has: only the guides folder, not all of ./docs.needs [ read [./docs] ]
fn show(c, name) { print(name + ":", read_in(c, name) or "(missing)")}
let docs = read[./docs]let guides = demote(docs, read[./docs/guides])show(guides, "intro.md")show(guides, "nope.md")capabilities.sub
It can:├─ can read files│ ./docs and everything below it└─ cannot write files, run programs, use the network, read environment variables, read the clock, or use random numbers
The calls it makes: line 5 read_in(c, name) depends on a value, checked when it runsfix_tests.sub
Run the tests; when they fail, patch the failing file and commit the patch.
// Run the tests; when they fail, patch the failing file and commit the patch.needs [ read [./src], write [./src], run ["npm test", "git add ./src", "git commit -m *"],]
fn fix(text, output) { text + "// TODO: the tests said: " + trim(output) + "\n"}
let t = run("npm test")if (t.failed) { let path = ./src/index.ts let text = read(path)? write(path, fix(text, t.output))? run("git add ./src") run(["git", "commit", "-m", "fix failing test"])} else { print("all tests pass")}fix_tests.sub
It can:├─ can read files│ ./src and everything below it├─ can write files│ ./src and everything below it├─ can run programs│ git add ./src and any arguments after it│ git commit -m * exactly these arguments (a * is any one word)│ npm test and any arguments after it└─ cannot use the network, read environment variables, read the clock, or use random numbers
The calls it makes: line 12 run("npm test") allowed by `npm test` line 15 read(path) allowed by `./src` line 16 write(path, fix(text, t.output)) allowed by `./src` line 17 run("git add ./src") allowed by `git add ./src` line 18 run(["git", "commit", "-m", "fix failing test"]) allowed by `git commit -m *`github_status.sub
Ask the GitHub API how many requests are left. Needs a token in GITHUB_TOKEN.
// Ask the GitHub API how many requests are left. Needs a token in GITHUB_TOKEN.needs [ net ["api.github.com:443"], env ["GITHUB_TOKEN"],]
let token = env("GITHUB_TOKEN") or { print("set GITHUB_TOKEN first") return 1}match (fetch("https://api.github.com/rate_limit")) { Ok(body) => print("rate limit says:", len(body), "bytes"), Err(e) => print("could not reach GitHub:", e)}github_status.sub
It can:├─ can reach the network│ api.github.com port 443 only├─ can read environment variables│ GITHUB_TOKEN└─ cannot read files, write files, run programs, read the clock, or use random numbers
The calls it makes: line 7 env("GITHUB_TOKEN") allowed by `GITHUB_TOKEN` line 11 fetch("https://api.github.com/rate_limit") allowed by `api.github.com:443`release_notes.sub
Turn the git log into release notes. (Make ./out first.)
// Turn the git log into release notes. (Make ./out first.)needs [ run ["git log --oneline"], write [./out],]
let log = run("git log --oneline")if (log.failed) { print("git log failed:", log.error) return 1}let mut notes = "# Release notes\n\n"for (line in lines(log.output)) { notes = notes + "- " + line + "\n"}write(./out/NOTES.md, notes)?print("wrote ./out/NOTES.md with", len(lines(log.output)), "entries")release_notes.sub
It can:├─ can write files│ ./out and everything below it├─ can run programs│ git log --oneline and any arguments after it└─ cannot read files, use the network, read environment variables, read the clock, or use random numbers
The calls it makes: line 7 run("git log --oneline") allowed by `git log --oneline` line 16 write(./out/NOTES.md, notes) allowed by `./out`secrets.sub
A secret can be compared and passed on to something that takes a Secret, never printed.
// A secret can be compared and passed on to something that takes a Secret, never printed.needs [ env ["API_TOKEN"] ]
let token = env("API_TOKEN") or { print("API_TOKEN is not set") return 1}if (secret_eq(token, "")) { print("the token is empty")} else { print("a token is set")}secrets.sub
It can:├─ can read environment variables│ API_TOKEN└─ cannot read files, write files, run programs, use the network, read the clock, or use random numbers
The calls it makes: line 4 env("API_TOKEN") allowed by `API_TOKEN`todo_report.sub
Count the TODO lines in every file of ./src. The helper may only read the file it is given.
// Count the TODO lines in every file of ./src. The helper may only read the file it is given.needs [ read [./src] ]
fn todos_in(path) needs [read [path]] { let mut n = 0 for (line in lines(read(path) or "")) { if (contains(line, "TODO")) { n = n + 1 } } n}
let mut total = 0for (name in list_dir(./src) or []) { let file = join_path(./src, name) let n = todos_in(file) print(name, n) total = total + n}print("total TODO:", total)todo_report.sub
It can:├─ can read files│ ./src and everything below it└─ cannot write files, run programs, use the network, read environment variables, read the clock, or use random numbers
The calls it makes: line 6 read(path) allowed by `parameter path` line 15 list_dir(./src) allowed by `./src`types_tour.sub
A tour of the types: enums and match, failure families, refined types, options and lambdas.
// A tour of the types: enums and match, failure families, refined types, options and lambdas.type Port = Int where 1..=65535type Name = String where len 1..=8
enum Shape { Circle(Int), Rect(Int, Int), Empty }
error ConfigErrorerror Missing <: ConfigErrorerror Malformed(line: Int) <: ConfigError
fn area(s: Shape) -> Int { match (s) { Circle(r) => 3 * r * r, Rect(w, h) => w * h, Empty => 0 }}
fn parse_port(text) -> Result<Port, ConfigError> { if (text == "") { return Missing("no port given") } let n = int(text) or { return Malformed(1) } Port(n) or { return Malformed(2) }}
fn describe(text) { match (parse_port(text)) { Ok(p) => "port {p}", Err(Missing) => "missing", Err(ConfigError) => "bad config" }}
defer print("done")
let shapes = [Circle(2), Rect(3, 4), Empty]print(map(shapes, area))print(fold(shapes, 0, fn(total, s) { total + area(s) }))
let default_port: Port = 8080print(default_port + 1)print(describe("443"), describe(""), describe("x"), describe("0"))
let name: Name = "sub"let record = { name: name, port: default_port, ratio: 7 / 2.0 }let moved = { ..record, port: 9090 }print(record.port, moved.port, moved.ratio)
let found = find([1, 5, 8], fn(n) { n > 4 })print(unwrap_or(found, 0), unwrap_or(find([1], fn(n) { n > 4 }), -1))print(match (first([10, 20])) { Some(x) => x + 1, None => 0 })types_tour.sub
It can do nothing: the file has no `needs` block.warn_unhandled_failure.sub
Not errors: failures that would stop the script, or that nobody looks at.
// Not errors: failures that would stop the script, or that nobody looks at.needs [ read [./notes], write [./notes] ]
let text = read(./notes/today.md)print(len(text))write(./notes/copy.md, "x")let safe = read(./notes/today.md) or ""print(len(safe))warn_unhandled_failure.sub
It can:├─ can read files│ ./notes and everything below it├─ can write files│ ./notes and everything below it└─ cannot run programs, use the network, read environment variables, read the clock, or use random numbers
The calls it makes: line 4 read(./notes/today.md) allowed by `./notes` line 6 write(./notes/copy.md, "x") allowed by `./notes` line 7 read(./notes/today.md) allowed by `./notes`Scripts that fail on purpose
Section titled “Scripts that fail on purpose”Each of these breaks one rule. The compiler stops and says how to fix it.
fail_demote_more.sub
needs [ read [./docs] ]
let guides = read[./docs/guides]let everything = demote(guides, read[./docs])error[E0222]: `demote` can only hand over less, and `read ./docs` is not inside `read ./docs/guides` --> fail_demote_more.sub:4:18 |4 | let everything = demote(guides, read[./docs]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this would give more power = help: pick a smaller capability, for example one inside `read ./docs/guides`
warning[W0401]: `needs` allows `read` but the script never uses it --> fail_demote_more.sub:1:9 |1 | needs [ read [./docs] ] | ^^^^^^^^^^^^^ unused permission = help: remove `read` to ask for less
error: 1 error found and 1 warningfail_denied_run.sub
needs [ run ["git status"] ]
run("git status")run("git push origin main")error[E0201]: `run("git push origin main")` is not allowed by `needs` --> fail_denied_run.sub:4:1 |4 | run("git push origin main") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ this command is not in the run list = help: add `run ["git push origin main"]` to the `needs` block at the top of the file, or remove the call
error: 1 error foundfail_function_needs.sub
needs [ read [./cfg] ]
fn load(p) needs [read [p]] { write(p, "oops")}
fn wide() needs [read [./elsewhere]] { 1}
load(./cfg/app.toml)load(./secrets/key)error[E0201]: `write(p, "oops")` is not allowed by `needs` --> fail_function_needs.sub:4:3 |4 | write(p, "oops") | ^^^^^^^^^^^^^^^^ `needs` has no write permission = help: add `write [p]` to the `needs` of the function `load` (and to the script's `needs` if it is not there yet), or remove the call
error[E0223]: `wide` asks for `read ./elsewhere`, which the script's `needs` does not allow --> fail_function_needs.sub:7:11 |7 | fn wide() needs [read [./elsewhere]] { | ^^^^^^^^^^^^^^^^^^^^^^^^^^ bigger than the script's permissions = help: add `read [./elsewhere]` to the `needs` block at the top of the file, or remove the call
error[E0221]: calling `load` needs `read ./secrets/key`, which is not allowed here --> fail_function_needs.sub:12:1 |12 | load(./secrets/key) | ^^^^^^^^^^^^^^^^^^^ the function asks for more than this script has = help: add `read [./secrets/key]` to the `needs` block at the top of the file, or remove the call
warning[W0603]: the failure of this call is ignored (IoError): if it fails, nothing says so --> fail_function_needs.sub:11:1 |11 | load(./cfg/app.toml) | ^^^^^^^^^^^^^^^^^^^^ = help: add `?` to stop here when it fails, or `or { ... }` to say what to do
warning[W0603]: the failure of this call is ignored (IoError): if it fails, nothing says so --> fail_function_needs.sub:12:1 |12 | load(./secrets/key) | ^^^^^^^^^^^^^^^^^^^ = help: add `?` to stop here when it fails, or `or { ... }` to say what to do
error: 3 errors found and 2 warningsfail_match_incomplete.sub
enum Light { Red, Yellow, Green }
fn next(l: Light) { match (l) { Red => Green, Green => Yellow }}
let delay = match (first([1, 2])) { Some(n) => n}error[E0642]: this `match` does not cover: Yellow --> fail_match_incomplete.sub:4:3 |4 | match (l) { | ^^^^^^^^^^^ not every case is covered = help: add those cases, or end with `_ => ...` to cover the rest
error[E0642]: this `match` does not cover: None --> fail_match_incomplete.sub:10:13 |10 | let delay = match (first([1, 2])) { | ^^^^^^^^^^^^^^^^^^^^^^^ not every case is covered = help: add those cases, or end with `_ => ...` to cover the rest
error: 2 errors foundfail_needs_late.sub
let greeting = "hello"needs [ read [./src] ]print(greeting)error[E0114]: `needs` must be the first thing in the file --> fail_needs_late.sub:2:1 |2 | needs [ read [./src] ] | ^^^^^ this block is too late = help: move the `needs [ ... ]` block to the top of the file, above all other code
error: 1 error foundfail_no_needs.sub
No needs block: this script can do nothing, so reading a file is refused.
// No needs block: this script can do nothing, so reading a file is refused.let text = read(./notes.txt)print(text)error[E0201]: `read(./notes.txt)` is not allowed by `needs` --> fail_no_needs.sub:2:12 |2 | let text = read(./notes.txt) | ^^^^^^^^^^^^^^^^^ ./notes.txt is not covered by `needs` = help: this file has no `needs` block, so it can do nothing yet: add `needs [ read [./notes.txt] ]` at the top of the file, or remove the call
error: 1 error foundfail_outside_path.sub
needs [ read [./src] ]
print(read(./src/main.ts))print(read(../secrets.txt))print(read(./src/../.env))error[E0201]: `read(../secrets.txt)` is not allowed by `needs` --> fail_outside_path.sub:4:7 |4 | print(read(../secrets.txt)) | ^^^^^^^^^^^^^^^^^^^^ ../secrets.txt is not covered by `needs` = help: add `read [../secrets.txt]` to the `needs` block at the top of the file, or remove the call = note: that path is outside the folder the script runs in
error[E0201]: `read(./src/../.env)` is not allowed by `needs` --> fail_outside_path.sub:5:7 |5 | print(read(./src/../.env)) | ^^^^^^^^^^^^^^^^^^^ ./.env is not covered by `needs` = help: add `read [./.env]` to the `needs` block at the top of the file, or remove the call
error: 2 errors foundfail_refined_literal.sub
type Port = Int where 1..=65535type Id = String matching "^ses_[0-9A-Z]\{4\}$"
let ok: Port = 8080let too_big: Port = 70000let loose: Id = "session-1"
let n = int(args[0]) or 0let unchecked: Port = nerror[E0650]: 70000 is not a valid Port: it must be between 1 and 65535 --> fail_refined_literal.sub:5:21 |5 | let too_big: Port = 70000 | ^^^^^ does not fit = help: `Port` is declared with a value between 1 and 65535
error[E0650]: "session-1" is not a valid Id: it must match "^ses_[0-9A-Z]{4}$" --> fail_refined_literal.sub:6:17 |6 | let loose: Id = "session-1" | ^^^^^^^^^^^ does not fit = help: `Id` is declared with text matching "^ses_[0-9A-Z]{4}$"
error[E0610]: this value: expected Port, found Int --> fail_refined_literal.sub:9:23 |9 | let unchecked: Port = n | ^ this is Int = help: convert it with Port(x): it checks the value when the script runs and gives a failure if it does not fit
error: 3 errors foundfail_secret_leak.sub
needs [ env ["API_TOKEN"] ]
let token = env("API_TOKEN") or ""print("using token", token)let header = "Bearer {token}"error[E0302]: a secret cannot be passed to `print` --> fail_secret_leak.sub:4:22 |4 | print("using token", token) | ^^^^^ this is a secret = help: a secret can only be compared with secret_eq(a, b) or passed to your own function that takes a `Secret`
error[E0301]: a secret cannot be put into a string --> fail_secret_leak.sub:5:23 |5 | let header = "Bearer {token}" | ^^^^^ this is a secret = help: secrets can only be passed to functions that take a `Secret`, such as secret_eq(a, b)
error: 2 errors foundfail_shell_pipe.sub
needs [ run ["ls", "wc"] ]
let n = run("ls | wc -l")error[E0202]: `run` does not use a shell, so `|` has no meaning here --> fail_shell_pipe.sub:3:13 |3 | let n = run("ls | wc -l") | ^^^^^^^^^^^^ `|` would be passed to the program as plain text = help: run each program with its own `run(...)` and pass `.output` on; to give a program several arguments use a list: run(["git", "commit", "-m", message])
warning[W0402]: `ls` in `run` is never used --> fail_shell_pipe.sub:1:14 |1 | needs [ run ["ls", "wc"] ] | ^^^^ unused permission = help: remove it to ask for less
warning[W0402]: `wc` in `run` is never used --> fail_shell_pipe.sub:1:20 |1 | needs [ run ["ls", "wc"] ] | ^^^^ unused permission = help: remove it to ask for less
error: 1 error found and 2 warningsfail_syntax.sub
needs [ read [./src] ]
let text = read(./src/main.tsprint(text)error[E0110]: expected `,` or `)`, found the name `print` --> fail_syntax.sub:4:1 |4 | print(text) | ^^^^^ expected `,` or `)` here = help: separate items with commas, and close the list with `)` (opened on line 3)
error: 1 error foundfail_type_mismatch.sub
Types are inferred; these mistakes are found before the script runs.
// Types are inferred; these mistakes are found before the script runs.needs [ env ["API_TOKEN"] ]
let count: Int = "three"let label = "items: " + countlet total = len([1, 2]) + "2"let token = env("API_TOKEN") or { return 1 }let shown: String = tokenerror[E0610]: this value: expected Int, found String --> fail_type_mismatch.sub:4:18 |4 | let count: Int = "three" | ^^^^^^^ this is String = help: turn the text into a number with int(x), which gives a failure if it is not a number: `int(x)?` or `int(x) or 0`
error[E0630]: `+` needs two numbers, two lists, or text on the left: Int and String --> fail_type_mismatch.sub:6:13 |6 | let total = len([1, 2]) + "2" | ^^^^^^^^^^^^^^^^^ Int and String = help: turn a number into text with str(x) to add it to text
error[E0610]: this value: expected String, found Secret<String> --> fail_type_mismatch.sub:8:21 |8 | let shown: String = token | ^^^^^ this is Secret<String> = help: a secret is not a plain value: it can only be compared with secret_eq(a, b) or passed on to something that takes a Secret
error: 3 errors foundfail_typo.sub
needs [ read [./notes] ]
let count = len(lines(read(./notes/today.md) or ""))print("lines:", cuont)rad(./notes/today.md)error[E0310]: `cuont` is not defined --> fail_typo.sub:4:17 |4 | print("lines:", cuont) | ^^^^^ unknown name = help: did you mean `count`?
error[E0311]: there is no function called `rad` --> fail_typo.sub:5:1 |5 | rad(./notes/today.md) | ^^^ unknown function = help: did you mean `read`?
error: 2 errors found