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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Build, run and compare
run: sh tools/check.sh $(ls apps | grep -E -- '-(python|rust)$')

# The documentation tells readers that newer toolchains usually work. This
# The documentation says that newer toolchains usually work. This
# tests that claim nightly, against whatever those projects have released,
# with no cached interpreter so CPython is built from source as a reader
# would. Only wasi-sdk stays fixed, since a new one moves the sysroot the
Expand Down
24 changes: 17 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,32 @@ make build APP=<app> # one module into build/<app>.wasm
make run APP=<app> # build, then both passes against fixtures/
make run APP=<app> FIXTURES=fixtures/no-call
make run APP=<app> FIXTURES=fixtures/unanswered
OPTIONAL=off make run APP=<app> # as if the owner declined every optional grant
INPUTS=<dir> make run APP=<app> # answers from <dir>, not apps/<app>/inputs
sh tools/check.sh # what CI runs: links, builds, runs, ports
```

`make run` mounts only the paths an app's manifest declares, read-only, and
runs it with `/` as the data directory, the way an Ark does. The three fixture
roots are described in `fixtures/README.md`. An app whose grants a root lacks
stops before its run pass there, which is expected.
runs it with `/` as the data directory, the way an Ark does. An optional path
is mounted when the fixture root holds it, and an app's answers at `/inputs`
when it declares inputs. The three fixture roots are described in
`fixtures/README.md`. An app whose required grants a root lacks stops before
its report pass there, which is expected.

## Adding an app

- Copy the nearest example. One source file, in `apps/<nn>-<name>-<lang>/` for
a mini and `apps/<nn>-<name>/` for a full app, with a short `README.md` that
says what it shows and how to run it.
- The manifest names the app, its version and the narrowest grants that answer
the question. Spell every path exactly as `ark data paths` shows it, with
`v1/` kept and no trailing `/`. `docs/02-manifest.md` has the rules.
- The manifest gives the app's name as the owner should see it, its version and
the narrowest grants that answer the question. Spell every path exactly as
`ark data paths` shows it, with `v1/` kept and no trailing `/`. A full
`[listing]` ends it, with a purpose for every grant, and every language port
of an app prints the same one apart from `source`. `docs/02-manifest.md` has
the rules.
- An app that asks the owner something declares `[inputs.<name>]` tables and
keeps sample answers in its `inputs/` folder, one file per input with no
trailing newline, which `make run` mounts.
- Read grants as plain files. Absent means "not found" and is a result. Any
other error goes to standard error with a non-zero exit. The shapes a
genotype can take are in `docs/04-reading-data.md`.
Expand Down Expand Up @@ -62,7 +72,7 @@ Follow `docs/06-reports.md`. In short:
- Pick one voice and keep it. Light or serious, never both in one report.
Genomics words, never clinic words.
- Plain Markdown only. No images, HTML, footnotes, encoded content, dates or
run ids. Tables with as few columns as carry the evidence, rows about 80
task ids. Tables with as few columns as carry the evidence, rows about 80
characters.

## Never
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# make build APP=01-hello-rust build one app
# make run build and run every app
# make run APP=01-hello-rust build and run one app
# make run APP=02-permissions OPTIONAL=off decline optional datasets
# make run APP=03-sundae-order INPUTS=answers override apps/<app>/inputs
# make clean remove build outputs
#
# Every app builds to a single module at build/<app>.wasm, whatever its
Expand All @@ -13,12 +15,13 @@
# the data an Ark would mount. See docs/05-running.md.

FIXTURES ?= fixtures
INPUTS ?= apps/$*/inputs
BUILD ?= build
APPS := $(sort $(notdir $(wildcard apps/*)))
SELECTED := $(or $(strip $(APP)),$(APPS))

# The scripts read these, and any toolchain override, from the environment.
export BUILD WASMTIME WASI_SDK PYTHON TINYGO WASM_OPT
export BUILD WASMTIME WASI_SDK PYTHON TINYGO WASM_OPT OPTIONAL INPUTS

.PHONY: all build run clean list FORCE

Expand All @@ -39,7 +42,7 @@ build-%: FORCE
run-%: FORCE
@if [ -z "$(strip $(APP))" ]; then echo; echo "===== $* ====="; fi
@sh tools/build.sh "$*"
@sh tools/run.sh "$(BUILD)/$*.wasm" "$(FIXTURES)"
@sh tools/run.sh "$(BUILD)/$*.wasm" "$(FIXTURES)" "$(INPUTS)"

clean:
@rm -rf $(BUILD) apps/*/target apps/*/*.wasm
Expand Down
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Worked examples for writing apps that run on an Ark.
An Ark holds a person's genome on a device only they control. Apps never receive
a copy of that data. They are sent to it instead, and run in a deterministic
sandbox on the Ark with no network and no writable storage. An app reads the
data it was granted as plain files and prints a report. The owner approves every
run on their phone and decides whether to release the result. Because the
sandbox contains the code, anyone can write an app and anyone can run anyone
else's. The [whitepaper](https://dark.bio/whitepaper.pdf) lays out the trust
model.
data it was granted as plain files and prints a report. The owner approves each
app on their phone before it runs, and decides whether to release its report.
Because the sandbox contains the code, anyone can write an app and anyone can
run anyone else's. The [whitepaper](https://dark.bio/whitepaper.pdf) lays out
the trust model.

These examples teach that model one idea at a time, from printing a line to
scanning a whole call file. Every app is a single source file.
Expand All @@ -29,7 +29,7 @@ An app is a [WebAssembly](https://webassembly.org/) module built for
```rust
fn main() {
if std::env::args().nth(1).is_none() {
print!("[package]\nname = \"hello\"\nversion = \"0.1.0\"\ndatasets = []\n");
print!("manifest = 1\n\n[app]\nname = \"hello\"\nversion = \"0.1.0\"\n");
return;
}
println!("Hello from an Ark app.");
Expand All @@ -47,7 +47,7 @@ make run # every app
```

Run it on an Ark with the [`ark`](https://github.com/dark-bio/cli) tool. The
owner approves the run on their phone, in Ark Companion for
owner approves it on their phone, in Ark Companion for
[iOS](https://apps.apple.com/app/id6751324700) or
[Android](https://play.google.com/store/apps/details?id=bio.dark.companion).
Without hardware, [Ark Emulator](https://github.com/dark-bio/emulator) boots
Expand All @@ -67,30 +67,31 @@ what a laptop can't reproduce, and running on an Ark or an emulator.
Most data comes in two versions. A *mini* shows the bare read in a few lines,
and a full app turns the same read into a report. Each row is a folder under
`apps/`, with the language appended for the minis, such as
`03-cilantro-mini-rust`, and that folder name is what `APP=` takes.
`04-cilantro-mini-rust`, and that folder name is what `APP=` takes.

| App | What it shows | Languages |
| :-- | :-- | :-- |
| `01-hello` | the manifest pass and the run pass | Rust, Go, C, Python |
| `02-permissions` | requesting data, least privilege and the `develop` flag | Rust |
| `03-cilantro-mini` | one variant through `rsids/` | Rust, Go, C, Python |
| `03-cilantro-soapiness` | the same variant as a report | Rust |
| `04-drunk-o-type` | a panel of variants | Rust |
| `05-genes-mini` | one gene through `genes/` | Rust, Go, C, Python |
| `05-bitter-meter` | the gene and its changes as a report | Rust |
| `06-regions-mini` | an interval through `regions/` | Rust, Go, C, Python |
| `06-powerhouse-of-the-cell` | the mitochondrial genome as a report | Rust |
| `07-vcf-mini` | streaming the raw call file | Rust, Go, C, Python |
| `07-vcf-roll-call` | parsing the whole call file as a report | Rust |
| `08-motif-finder` | computing over a gene's reference sequence | Rust |
| `09-fortune-cookie` | why the sandbox is deterministic | Rust |
| `01-hello` | the manifest pass and the report pass | Rust, Go, C, Python |
| `02-permissions` | requesting data, optional grants, least privilege and the `develop` flag | Rust |
| `03-sundae-order` | asking the owner questions with inputs | Rust |
| `04-cilantro-mini` | one variant through `rsids/` | Rust, Go, C, Python |
| `04-cilantro-soapiness` | the same variant as a report | Rust |
| `05-drunk-o-type` | a panel of variants | Rust |
| `06-genes-mini` | one gene through `genes/` | Rust, Go, C, Python |
| `06-bitter-meter` | the gene and its changes as a report | Rust |
| `07-regions-mini` | an interval through `regions/` | Rust, Go, C, Python |
| `07-powerhouse-of-the-cell` | the mitochondrial genome as a report | Rust |
| `08-vcf-mini` | streaming the raw call file | Rust, Go, C, Python |
| `08-vcf-roll-call` | parsing the whole call file as a report | Rust |
| `09-motif-finder` | computing over a gene's reference sequence | Rust |
| `10-fortune-cookie` | why the sandbox is deterministic | Rust |

## Documentation

- [01-app-model.md](docs/01-app-model.md) covers the two passes, the checks, the
sandbox and its limits.
- [02-manifest.md](docs/02-manifest.md) covers the manifest and how to grant
data.
- [02-manifest.md](docs/02-manifest.md) covers the manifest, how to grant data
and how to ask the owner questions.
- [03-data-paths.md](docs/03-data-paths.md) maps the data tree and the rules
every path follows.
- [04-reading-data.md](docs/04-reading-data.md) covers absence, errors,
Expand All @@ -103,7 +104,7 @@ and a full app turns the same read into a report. Each row is a folder under
## A note on scope

These apps show how to read data and present a result. Their trait readouts are
illustrations, not medical advice, and one variant rarely tells a whole story.
illustrations, not medical advice, and one variant is rarely the whole story.

## License

Expand Down
2 changes: 1 addition & 1 deletion apps/01-hello-c/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ To see each pass by hand, build the module and run it twice:
```sh
make build APP=01-hello-c
wasmtime build/01-hello-c.wasm # manifest pass
wasmtime build/01-hello-c.wasm / # run pass
wasmtime build/01-hello-c.wasm / # report pass
```
30 changes: 23 additions & 7 deletions apps/01-hello-c/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,34 @@

int main(int argc, char *argv[]) {
(void)argv;
// The manifest pass: no data directory was given, so describe the app and
// stop. This app reads nothing, so its dataset list is empty.
// The manifest pass has no data directory, so describe the app and stop.
// This app reads nothing, so it has no reads table.
if (argc < 2) {
printf(
"[package]\n"
"name = \"hello-c\"\n"
"version = \"0.1.0\"\n"
"datasets = []\n");
"manifest = 1\n\n"
"[app]\n"
"name = \"Hello, Ark\"\n"
"version = \"0.1.0\"\n\n"
"[listing]\n"
"language = \"en\"\n"
"icon = \"👋\"\n"
"summary = \"The two passes of an app, in the fewest lines. Start here.\"\n"
"category = \"developer\"\n"
"license = \"BSD-3-Clause\"\n"
"source = \"https://github.com/dark-bio/examples/tree/main/apps/01-hello-c\"\n"
"keywords = [\"tutorial\", \"manifest\", \"two passes\", \"WASI\"]\n"
"description = '''\n"
"An Ark app is one WebAssembly file built for WASI Preview 1. The Ark runs it twice.\n\n"
"**The manifest pass.** With no arguments, the app prints a short TOML manifest naming itself, its version and the data it wants. The Ark checks the manifest and shows it on your phone.\n\n"
"**The report pass.** With a data directory as its first argument, the app reads the files it was granted and prints its report. Reports are Markdown, which is how Ark Hub shows them.\n\n"
"This app asks for no data, so approving it grants nothing. It proves the round trip, from upload through approval to a rendered report, in the fewest lines possible. Pick a language, read the source, then run it on your Ark.\n\n"
"Each language's folder in the examples repository builds and runs it with one `make run` command.\n\n"
"The docs cover the two passes, the manifest and the sandbox limits in detail.\n"
"'''\n");
return 0;
}

// The run pass: a data directory was given. A real app would read it here.
// The report pass receives a data directory. This app reads nothing.
printf("Hello from an Ark app written in C.\n");
return 0;
}
2 changes: 1 addition & 1 deletion apps/01-hello-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ To see each pass by hand, build the module and run it twice:
```sh
make build APP=01-hello-go
wasmtime build/01-hello-go.wasm # manifest pass
wasmtime build/01-hello-go.wasm / # run pass
wasmtime build/01-hello-go.wasm / # report pass
```
30 changes: 23 additions & 7 deletions apps/01-hello-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,33 @@ import (
)

func main() {
// The manifest pass: no data directory was given, so describe the app and
// stop. This app reads nothing, so its dataset list is empty.
// The manifest pass has no data directory, so describe the app and stop.
// This app reads nothing, so it has no reads table.
if len(os.Args) < 2 {
fmt.Print(
"[package]\n" +
"name = \"hello-go\"\n" +
"version = \"0.1.0\"\n" +
"datasets = []\n")
"manifest = 1\n\n" +
"[app]\n" +
"name = \"Hello, Ark\"\n" +
"version = \"0.1.0\"\n\n" +
"[listing]\n" +
"language = \"en\"\n" +
"icon = \"👋\"\n" +
"summary = \"The two passes of an app, in the fewest lines. Start here.\"\n" +
"category = \"developer\"\n" +
"license = \"BSD-3-Clause\"\n" +
"source = \"https://github.com/dark-bio/examples/tree/main/apps/01-hello-go\"\n" +
"keywords = [\"tutorial\", \"manifest\", \"two passes\", \"WASI\"]\n" +
"description = '''\n" +
"An Ark app is one WebAssembly file built for WASI Preview 1. The Ark runs it twice.\n\n" +
"**The manifest pass.** With no arguments, the app prints a short TOML manifest naming itself, its version and the data it wants. The Ark checks the manifest and shows it on your phone.\n\n" +
"**The report pass.** With a data directory as its first argument, the app reads the files it was granted and prints its report. Reports are Markdown, which is how Ark Hub shows them.\n\n" +
"This app asks for no data, so approving it grants nothing. It proves the round trip, from upload through approval to a rendered report, in the fewest lines possible. Pick a language, read the source, then run it on your Ark.\n\n" +
"Each language's folder in the examples repository builds and runs it with one `make run` command.\n\n" +
"The docs cover the two passes, the manifest and the sandbox limits in detail.\n" +
"'''\n")
return
}

// The run pass: a data directory was given. A real app would read it here.
// The report pass receives a data directory. This app reads nothing.
fmt.Println("Hello from an Ark app written in Go.")
}
2 changes: 1 addition & 1 deletion apps/01-hello-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ To see each pass by hand, build the module and run it twice:
```sh
make build APP=01-hello-python
wasmtime build/01-hello-python.wasm # manifest pass
wasmtime build/01-hello-python.wasm / # run pass
wasmtime build/01-hello-python.wasm / # report pass
```
30 changes: 28 additions & 2 deletions apps/01-hello-python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,36 @@
def main():
# With no data directory, print the manifest and stop.
if len(sys.argv) < 2:
print('[package]\nname = "hello-python"\nversion = "0.1.0"\ndatasets = []')
print("""manifest = 1

[app]
name = "Hello, Ark"
version = "0.1.0"

[listing]
language = "en"
icon = "👋"
summary = "The two passes of an app, in the fewest lines. Start here."
category = "developer"
license = "BSD-3-Clause"
source = "https://github.com/dark-bio/examples/tree/main/apps/01-hello-python"
keywords = ["tutorial", "manifest", "two passes", "WASI"]
description = '''
An Ark app is one WebAssembly file built for WASI Preview 1. The Ark runs it twice.

**The manifest pass.** With no arguments, the app prints a short TOML manifest naming itself, its version and the data it wants. The Ark checks the manifest and shows it on your phone.

**The report pass.** With a data directory as its first argument, the app reads the files it was granted and prints its report. Reports are Markdown, which is how Ark Hub shows them.

This app asks for no data, so approving it grants nothing. It proves the round trip, from upload through approval to a rendered report, in the fewest lines possible. Pick a language, read the source, then run it on your Ark.

Each language's folder in the examples repository builds and runs it with one `make run` command.

The docs cover the two passes, the manifest and the sandbox limits in detail.
'''""")
return

# The run pass receives a data directory, even when no data is requested.
# The report pass receives a data directory, even when no data is requested
print("Hello from an Ark app written in Python.")


Expand Down
2 changes: 1 addition & 1 deletion apps/01-hello-rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ To see each pass by hand, build the module and run it twice:
```sh
make build APP=01-hello-rust
wasmtime build/01-hello-rust.wasm # manifest pass
wasmtime build/01-hello-rust.wasm / # run pass
wasmtime build/01-hello-rust.wasm / # report pass
```
38 changes: 31 additions & 7 deletions apps/01-hello-rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,42 @@
//! ../../docs/01-app-model.md for the two passes.

fn main() {
// The manifest pass: no data directory was given, so describe the app and
// stop. This app reads nothing, so its dataset list is empty.
// The manifest pass has no data directory, so describe the app and stop.
// This app reads nothing, so it has no reads table.
if std::env::args().nth(1).is_none() {
print!(
"[package]\n\
name = \"hello-rust\"\n\
version = \"0.1.0\"\n\
datasets = []\n"
r#"manifest = 1

[app]
name = "Hello, Ark"
version = "0.1.0"

[listing]
language = "en"
icon = "👋"
summary = "The two passes of an app, in the fewest lines. Start here."
category = "developer"
license = "BSD-3-Clause"
source = "https://github.com/dark-bio/examples/tree/main/apps/01-hello-rust"
keywords = ["tutorial", "manifest", "two passes", "WASI"]
description = '''
An Ark app is one WebAssembly file built for WASI Preview 1. The Ark runs it twice.

**The manifest pass.** With no arguments, the app prints a short TOML manifest naming itself, its version and the data it wants. The Ark checks the manifest and shows it on your phone.

**The report pass.** With a data directory as its first argument, the app reads the files it was granted and prints its report. Reports are Markdown, which is how Ark Hub shows them.

This app asks for no data, so approving it grants nothing. It proves the round trip, from upload through approval to a rendered report, in the fewest lines possible. Pick a language, read the source, then run it on your Ark.

Each language's folder in the examples repository builds and runs it with one `make run` command.

The docs cover the two passes, the manifest and the sandbox limits in detail.
'''
"#
);
return;
}

// The run pass: a data directory was given. A real app would read it here.
// The report pass receives a data directory. This app reads nothing.
println!("Hello from an Ark app written in Rust.");
}
2 changes: 1 addition & 1 deletion apps/02-permissions/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/02-permissions/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "permissions"
version = "0.1.0"
version = "0.2.0"
edition = "2024"

# A smaller module uploads and starts faster on an Ark.
Expand Down
Loading
Loading