Unzips a bundle built by mkbndl() into a new
folder, runs it the way a recipient would
(Rscript setup_and_run.R --quick) and reads the
manifest.json the run writes. The bundle passes when the run
finishes, writes its manifest, no cross-check is DIFFER and, on real
data, the number of checks and of passes is the one the expected
block of its config.json gives (pass_csv for a CSV input,
pass_real for the .RData). A run on synthetic data marks
every check INFO, so it is not compared with expected. A launcher
(start_here.sh, START_HERE.command) the zip does not mark
executable is reported as a warning: it still runs as
bash start_here.sh.
Usage
mkbndlchk(
bundle,
data = NULL,
synthetic = FALSE,
dir = NULL,
keep = FALSE,
container = NULL
)Arguments
- bundle
Path to the bundle zip.
- data
Optional path to the data file, passed to the run as
--data.NULL(default): the bundle finds the file, downloads it or, withsynthetic = TRUE, generates data.- synthetic
Run on synthetic data (
--synthetic), defaultFALSE; needs no data file and no network.- dir
A folder to unzip into, which must not exist yet. Default: a new folder under
tempdir().- keep
Keep that folder after the check, default
FALSE.- container
Run the bundle in a container:
NULLorFALSE(default) do not,TRUEuses podman, else docker, and"podman"or"docker"names the runtime. See Details.
Value
An object of class rmbl_bundle_audit: a list with
ok, checks (a data frame of check, ok,
detail and severity), mode (the input the run
used, from its manifest: synthetic when it is marked
synthetic, else its meta$input_mode when it records one, such
as csv, else real), counts (total, pass,
differ, info), log (the last 30 lines the run
printed), isolation (how the run was contained: see Details),
bundle and dir (the unzipped bundle, or for a container
run the folder it wrote into; NA once removed). It prints as a
box.
Details
Warning: this runs the bundle's code on your machine, with your
user's rights: its setup_and_run.R, its analysis script and
anything they start. Check only bundles you built or whose author you
trust. To limit what a hostile bundle can reach, the run gets a minimal
environment: PATH, the locale variables (LANG,
LANGUAGE, LC_ALL and the LC_ categories such as
LC_CTYPE, no other LC_ name; TZ), the library paths
of this session (R_LIBS), the proxy variables
(http_proxy, https_proxy, no_proxy) when they hold
no user name or password, a fresh temporary HOME and
TMPDIR, and no other variable, so API keys, tokens and passwords
in the environment (or in ~/.Renviron) do not reach it. On
Windows the variables that look like credentials are removed instead (a
best effort): names holding KEY, TOKEN, SECRET,
PASS, CREDENTIAL, AUTH, COOKIE or
SESSION, a service's prefix (GITHUB_, AWS_, ...),
connection strings (DATABASE_URL, *_DATABASE_URL,
*_WEBHOOK_URL, SENTRY_DSN, *_DSN), *_PWD,
*_SK, other
LC_ names, and any value that is a URL with a password in it.
This is not a sandbox: the files your user can read stay readable.
The run is stopped after getOption("rmbl.bundle.timeout")
seconds (default 1800, 30 minutes; the exit status is then 124). How
the processes it starts are contained (the isolation of the
result, and a row of the checks):
- container
with
container = TRUE(podman, else docker) or a runtime named: the run in a container, removed afterwards, with the unzipped bundle mounted read-only and copied into a writable folder, no network unless the real data is to be downloaded (nodata, not synthetic), 4 GB of memory, 2 CPUs, 512 processes, a read-only root file system, no capabilities and your user id. The image isgetOption("rmbl.bundle.image"), default R 4.6.1 from the Rocker project pinned by digest; it holds base R only, so a bundle that lists packages inr_packages(or downloads its data, which needs rmoriebricklayer) needs an image that has them. Without the runtime the check falls back to the next mode and says so.- pid namespace
Linux, when
unsharecan make an unprivileged user and PID namespace: every process the run started, also one that started its own session or forked twice, is killed when the run ends or times out.- job object
Windows: the run is started inside a job object that kills every process in it when the run ends or times out.
- process group
Linux without user namespaces, and macOS: the run leads a process group, killed when it ends, times out or is interrupted. A process can leave the group (setsid, a double fork) and clear its environment. On Linux this R session is made the run's child subreaper, so such a process is re-parented to it rather than to init: every process the run left behind is found by the process tree, killed by its PID and fails the check (the isolation row is an error). Where that cannot be done (macOS, or a Linux kernel older than 3.4) escaped processes cannot be detected, and the isolation row fails: use
container = TRUEthere.
Only a results_* folder the run itself created counts: one the
zip already carries (which mkbndl() never packs) is not read, and
fails the check. A run that writes no manifest, or whose runner reports
“NOT completed”, fails.
Errors: bundle that is not an existing .zip file, or not
a zip this function reads (more than 65535 entries, an entry name with a
NUL byte, one too long, absolute or leaving the folder, or an entry
given twice); a zip without setup_and_run.R; data that is
not an existing file; dir that already exists.
See also
mkbndl() builds the bundle;
mkbndlproj() starts a project.
Examples
proj <- file.path(tempdir(), "audit-demo")
mkbndlproj(proj, title = "Demo analysis")
#> ╭── Bundle project ───────────────────────────────────────────────────────────╮
#> │ Folder /tmp/RtmpZpRdcE/audit-demo │
#> │ Files config.json, data_provenance.json, analysis.R │
#> │ │
#> │ Edit config.json (who and what), data_provenance.json (the data file, │
#> │ its URL and SHA-256) and analysis.R (the checks), then build the zip │
#> │ with mkbndl(). │
#> │ It runs as it is on synthetic data, so the bundle can be tried before │
#> │ the analysis is written. │
#> ╰─────────────────────────────────────────────────────────────────────────────╯
b <- mkbndl(proj, dest = tempdir(), overwrite = TRUE)
a <- mkbndlchk(b$path, synthetic = TRUE)
a
#> ╭── Bundle audit: VERIFIED ───────────────────────────────────────────────────╮
#> │ ✔ VERIFIED · 7 of 7 checks passed │
#> │ │
#> │ Bundle audit-demo_v1.zip │
#> │ Input mode synthetic │
#> │ Isolation process group │
#> │ Checks 2 │
#> │ ✔ 0 │
#> │ DIFFER 0 │
#> │ INFO 2 │
#> │ │
#> │ Every check passed: the bundle runs as a recipient's copy will and is │
#> │ safe to ship. │
#> │ │
#> │ ✔ run finished exit status 0 │
#> │ ✔ manifest written │
#> │ /tmp/RtmpZpRdcE/bndlchk1e6af96d8b3/audit-demo_v1/results_20261030-17 │
#> │ 2533/manifest.json │
#> │ ✔ no cross-check differs 0 DIFFER │
#> │ ✔ counts match config.json not compared: a synthetic run │
#> │ ✔ launchers executable 2 launcher(s) │
#> │ ✔ no results packed in the zip none │
#> │ ✔ isolation process group: no process outlived the run │
#> │ │
#> ╰─────────────────────────────────────────────────────────────────────────────╯
a$counts
#> total pass differ info
#> 2 0 0 2
