--- title: "Policies and Channels" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Policies and Channels} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(getaca) .old_options <- options(getaca.cache = file.path(tempdir(), "getaca-policies")) .old_envvars <- Sys.getenv(c("GETACA_OFFLINE", "NOT_CRAN"), unset = NA) Sys.setenv(GETACA_OFFLINE = "true", NOT_CRAN = "true") ``` A resolution policy answers one question: which registry state does a name resolve through? Everything downstream of that answer deals in immutable records, so the policy is the only place where "the same call on two days" can legitimately produce different bytes. This article covers the four policies, what a remote channel is allowed to change, how pinning freezes an analysis, and how the three settings layers combine. ## The two halves the design keeps apart A **resource record** names exact bytes and never changes: ```{r} rec <- resource("backbone", "2026-06", urls = "https://host.invalid/backbone-2026-06.zip", sha256 = strrep("9f", 32), license = "CC-BY-4.0") format(rec) ``` A **channel** maps the logical name `"backbone"` onto one record, and channels move. Three places a channel can be read from, so three ways the same name can resolve: | Policy | Channel comes from | Same bytes forever? | |---|---|---| | `bundled` | the registry inside the installed package | yes, for that install | | `current` | the author's remote registry, bundled as fallback | no, by design | | `pinned` | a frozen local snapshot | yes, until you re-pin | | `offline` | the bundled registry, network never touched | yes, for that install | `bundled` and `offline` read the same channel. They differ in what happens when the resource is absent from the cache: `bundled` downloads it, `offline` raises `getaca_error_offline` and says how to prefetch. ## `bundled` The default, and the one that makes a data dependency behave like a code dependency. The registry that shipped inside the installed package is the whole answer; nothing is fetched to decide what a name means. ```{r} reg <- registry( package = "yourpkg", resources = list(rec) ) res <- resolve_resource("backbone", registry = reg, policy = "bundled") res$id res$source res$digest ``` `source` and `digest` are recorded in the cache entry, so a provenance report can state which declaration chose the bytes and which policy read it. The digest comes from the channel that answered, so under `current` it names the remote state rather than the registry the call was handed. Reinstalling the package is what moves this channel. That is the property worth having: two machines running the same installed version resolve the same record, and an analysis is reproducible from the package version alone. ## `current` For data that release on their own schedule. The author publishes a static registry file somewhere they control, and `getaca` consults it before resolving: ```{r} remote_reg <- registry( package = "yourpkg", policy = "current", remote = "https://yourpkg.invalid/getaca-registry.rds", resources = list(rec) ) remote_reg ``` The remote is a file, not a service. Publishing it is a `registry_write()` into a directory that GitHub Pages, r-universe or an institutional web server already serves. An unreachable remote produces a message and the bundled registry, which is what CRAN policy requires of a package that uses Internet resources: ```{r, eval = FALSE} resolve_resource("backbone", registry = remote_reg, policy = "current") #> getaca: could not reach the remote registry for 'yourpkg'; using the bundled #> registry. ``` An unreadable remote behaves the same way. A file that arrives but does not parse as a registry, or parses but fails validation, produces a message and the bundled fallback rather than a hard failure. A remote that declares a different package is refused outright, since that is a misconfiguration rather than an outage. The remote registry is fetched once per session and cached, so a script making a hundred calls performs one fetch. `getaca_refresh()` forgets it, which is how you pick up a registry you published five minutes ago without restarting. ```{r} getaca_refresh() ``` That also forgets registries discovered from installed packages, so it is what you call after installing a new version of a declaring package mid-session. Cached *resources* are untouched; this forgets declarations, not data. ### What a remote channel may change Two things: - **mirrors**, for records that already exist. A dead host is repaired by publishing a registry whose record for `2026-06` lists a new URL. - **records**, by adding new ones and moving the head onto them. One thing it may not change: the checksum attached to a version that already exists. That is refused, and the error names the declaring package as the party at fault rather than the publisher. The same rule holds for a pin file, which is the branch that can be shown here without a network: ```{r} rewritten <- file.path(tempdir(), "rewritten.rds") saveRDS( list(yourpkg = registry("yourpkg", list( resource("backbone", "2026-06", urls = "https://host.invalid/backbone-2026-06.zip", sha256 = strrep("ab", 32), license = "CC-BY-4.0") ))), rewritten ) options(getaca.pin_file = rewritten) ``` ```{r, error = TRUE} resolve_resource("backbone", registry = reg, policy = "pinned") ``` ```{r, include = FALSE} options(getaca.pin_file = NULL) ``` Without that rule, one edited file would be enough to change what a published version means, and the guarantee the design exists to provide would go with it. A publisher who genuinely recut a file gets a new version label, which is a record users can see and choose. ### Moving the head Publishing `2026-09` and moving the channel head onto it is one registry: ```{r} ahead <- registry( package = "yourpkg", current = c(backbone = "2026-09"), resources = list( resource("backbone", "2026-06", urls = "https://host.invalid/backbone-2026-06.zip", sha256 = strrep("9f", 32), license = "CC-BY-4.0"), resource("backbone", "2026-09", urls = "https://host.invalid/backbone-2026-09.zip", sha256 = strrep("ab", 32), license = "CC-BY-4.0") ) ) resolve_resource("backbone", registry = ahead)$id resolve_resource("backbone", registry = ahead, version = "2026-06")$id ``` Users on `current` follow the head on their next session. Users on `bundled` stay where their installed package put them. Users who named a version keep that version under every policy, which is the escape hatch an analysis needs. ### Signing a remote channel The rules above bound what a remote registry can do. They say nothing about who wrote it, and a registry sits on a host that can change hands. Signing answers that, and it works because of where the key lives: the public key travels inside the registry your package *ships*, which a user installs from CRAN, while the remote registry comes from your own host. Someone who takes the host does not thereby have the key. Make a key once, and keep the secret half outside the package source tree: ```{r} secret <- file.path(tempdir(), "yourpkg-signing.key") public <- registry_keygen(secret) substr(public, 1, 24) ``` Declare the public half in the registry the package ships: ```{r} signed <- registry( package = "yourpkg", remote = "https://host.invalid/yourpkg.rds", keys = public, resources = list( resource("backbone", "2026-06", urls = "https://host.invalid/backbone-2026-06.zip", sha256 = strrep("9f", 32), license = "CC-BY-4.0") ) ) ``` Then write and sign whatever you publish. Sign after writing: writing is what stamps the publication time, and the signature binds it. ```{r} path <- file.path(tempdir(), "yourpkg.rds") registry_write(signed, path) registry_sign(path, key = secret) cat(readLines(paste0(path, ".sig"))[1:4], sep = "\n") ``` Upload the `.sig` beside the registry; getaca fetches it from the registry's own URL with `.sig` appended. `registry_verify()` runs the same check a user's session will, which is worth doing before publishing: ```{r} registry_verify(path) ``` Editing the registry after signing breaks the link, which is the whole point: ```{r, error = TRUE} moved <- signed moved$resources[[1]]$urls <- "https://elsewhere.invalid/backbone-2026-06.zip" saveRDS(moved, path, version = 3) registry_verify(path) ``` Three consequences worth knowing before you declare a key: - **It is a commitment.** Once your shipped registry names a key, an unsigned or unverifiable remote is refused rather than used. Users are not stranded, since the bundled declaration still resolves, but the remote channel stops working until the signature does. - **A signature expires.** `registry_sign()` dates it 90 days out by default. Re-signing an unchanged registry extends it, and is the routine maintenance this feature asks of you. `expires = NA` opts out and leaves nothing bounding how long a stale declaration can be served in your name. - **Rotating a key needs a release.** The keys getaca trusts are the ones in the installed package, so publish the new key beside the old one, keep signing with the old, release, and retire the old key once that release is out. A package that declares no keys behaves exactly as it always did, and never fetches a signature at all. ## `pinned` A pin file records, for each named package, the registry state in force at the moment you pinned. Under the `pinned` policy that snapshot is what resolution reads, so an analysis keeps resolving what it was written against even after the package is reinstalled and the remote registry has moved on. ```{r} pins <- file.path(tempdir(), "getaca.pins.rds") saveRDS(list(yourpkg = registry("yourpkg", list(rec))), pins) options(getaca.pin_file = pins) ``` ```{r} moved_on <- registry( package = "yourpkg", current = c(backbone = "2026-09"), resources = list( rec, resource("backbone", "2026-09", urls = "https://host.invalid/backbone-2026-09.zip", sha256 = strrep("ab", 32), license = "CC-BY-4.0") ) ) resolve_resource("backbone", registry = moved_on)$id resolve_resource("backbone", registry = moved_on, policy = "pinned")$id ``` The installed registry has moved to `2026-09`; the pin holds the analysis at `2026-06`. In practice the pin file is written rather than hand-built: ```{r, eval = FALSE} getaca_pin(c("yourpkg", "otherpkg")) ``` That writes `getaca.pins.rds` in the working directory by default, which puts it beside `renv.lock` in a project. Under `current`, `getaca_pin()` records the remote state rather than the bundled one, so pinning captures what you are actually resolving through at that moment. Point it somewhere else with the `path` argument, and tell a session where to look with the `getaca.pin_file` option. A pin file is subject to the same immutability rule as a remote registry. A snapshot that disagrees with the installed registry about what `2026-06` means is refused rather than trusted, so a stale or edited pin cannot quietly serve different bytes. Pinning a package that ships no registry warns and skips it, so pinning a project's whole dependency list does not fail on the ones that need nothing: ```{r} getaca_pin("stats", path = file.path(tempdir(), "pins.rds")) ``` ```{r, include = FALSE} options(getaca.pin_file = NULL) ``` ### Pins and `renv` They answer adjacent questions. `renv` records which package versions an analysis used; a `getaca` pin records which data versions those packages resolved to. Together they pin both halves. Neither subsumes the other, because a package version does not determine a data version once the package is on `current`, and a data version says nothing about the code that read it. Commit both files. Restoring is `renv::restore()` followed by `getaca_prefetch()` on a connected machine, after which the analysis runs offline. ## `offline` Never reaches for the network. Cached copies and bundled declarations only. ```{r} err <- tryCatch(getaca("backbone", registry = reg, policy = "offline"), getaca_error = function(e) e) class(err)[1] ``` ```{r} cat(conditionMessage(err)) ``` This is the policy a check run gets, whatever is set elsewhere, and it is worth setting deliberately in two other situations: an air-gapped machine, where the message is more useful than a transfer timeout, and a shared analysis where you want a missing input to fail loudly rather than pull four gigabytes onto someone's laptop. ## How the policy in force is decided Four layers, most specific first: 1. the `policy` argument to `getaca()` or `resolve_resource()` 2. the `getaca.policy` option, set by `getaca_policy()` 3. the `GETACA_POLICY` environment variable 4. the declaring registry's own `policy` field, defaulting to `bundled` Two overrides sit above all of them and force `offline`: - `R CMD check`, detected from the check environment variables - `GETACA_OFFLINE` set to `1`, `true` or `yes` ```{r} getaca_policy() ``` The check clamp is released by `NOT_CRAN=true`, which is how a package's own CI runs tests against real resources while a CRAN machine never does. ```{r, eval = FALSE} getaca_policy("current") # this session getaca("backbone", package = "yourpkg", policy = "bundled") # this call Sys.setenv(GETACA_POLICY = "pinned") # this process ``` Setting the policy per call is the form worth reaching for in package code. A function that must not vary its answer can pass `policy = "bundled"` explicitly and stop caring what the user's session is set to. ## Choosing a default as an author The registry's `policy` field is your recommendation, and users can override it. Three questions decide it. **Do the data release independently of your package?** If a new release appears twice a year and your CRAN releases are annual, `current` is what keeps users on data you consider correct without a release cycle in between. If the data are static, `bundled` costs nothing and removes a moving part. **Can you host a registry file reliably enough?** Reliably here is a low bar, since an outage falls back to bundled with a message. GitHub Pages is sufficient. If you have nowhere to put a file you control, `bundled` is the honest choice. **Would a user be surprised?** A package whose results shift because the data moved needs that to be visible. `current` is right when the declaring package surfaces the resolved version in its own output, and questionable when it does not. `bundled` is the default for a reason. Under it a data dependency behaves the way a package dependency behaves: the installed version determines the answer. Each of the others relaxes that in one direction, and the direction is the reason to pick it. ## What is recorded Whichever policy resolved a resource is kept in its cache entry, alongside the digest of the registry state that supplied the record: ```{r, eval = FALSE} getaca_info("backbone", package = "yourpkg") #> yourpkg/backbone@2026-06 #> ... #> resolved by current registry sha256:8b31e0da54cf (published 2026-07-22) #> source url https://host.invalid/backbone-2026-06.zip ``` `getaca_catalogue()` reports the same two fields as `source` and `registry_digest` columns across every cached resource, so "which of these came from a remote channel" is one filter rather than an audit. ## Where to go next - `vignette("declaring")` for publishing a remote registry and moving a head - `vignette("checks")` for the check clamp, `NOT_CRAN` and CI - `vignette("failures")` for the conditions each policy can raise ```{r, include = FALSE} options(.old_options) Sys.unsetenv(names(.old_envvars)[is.na(.old_envvars)]) .restore <- .old_envvars[!is.na(.old_envvars)] if (length(.restore)) do.call(Sys.setenv, as.list(.restore)) ```