Toolboxes
What is a toolbox?
A Thorium toolbox is a portable, self-contained collection of tools — image and pipeline
configurations bundled together so they can be shared between Thorium instances. A toolbox is
described by a single toolbox.json manifest that lists every image and pipeline (along with
their Thorium configs), optionally alongside the container image files themselves for offline
transfer.
thorctl toolbox provides the full lifecycle for working with toolboxes:
| Command | Purpose |
|---|---|
thorctl toolbox init | Scaffold config.toml, image, and pipeline files for a new toolbox |
thorctl toolbox build | Generate a toolbox.json from a directory of manifests |
thorctl toolbox export | Pull images/pipelines out of a running Thorium instance into a toolbox |
thorctl toolbox import | Import a toolbox.json (local file or URL) into a Thorium instance |
thorctl toolbox remove | Delete a previously imported toolbox’s pipelines and images |
thorctl toolbox diff | Show what importing an on-disk toolbox would change, git-diff style |
thorctl toolbox build-images | Build (and --push) the container images in a toolbox locally |
Which command/flag for which task
| You want to… | Use |
|---|---|
| Start a brand-new toolbox by hand | init toolbox (creates config.toml + tool stubs), then build |
| Add one image/pipeline stub to an existing toolbox | init image/init pipeline (positional path = where it lands); pass -c <config.toml> to resolve/validate against the toolbox without moving files |
| Capture tools from a running instance | export (writes the whole repo + toolbox.json) |
| Append more tools into an existing toolbox repo | export -p …/-i … with -o <toolbox-dir> or -c <toolbox-dir>/config.toml (which defaults the output to that dir); the existing config.toml is reused automatically |
| Refresh every tool already in a toolbox from the instance | export -c <toolbox-dir>/config.toml --overwrite (no -g/-p/-i) — re-pulls and updates all existing tools in place |
Seed a new toolbox’s settings from another toolbox’s config.toml into a different dir | init toolbox -c …, or export -c <other>/config.toml -o <new-dir> (explicit -o required) |
| Place one resource’s files in a specific dir | export -i group/name=dest / -p group/name=dest (placement only) |
| Fold an exported image into an existing Dockerfile dir | export -i group/name=<dir-with-Dockerfile> (auto-sets build = true) |
| Set the default export layout | init toolbox --image-path … --pipeline-path … (writes export_*_path in config.toml) |
| Update a matched tool that differs (export) | --overwrite — updates in place (image: Thorium config only, keeps manifest.toml build settings); without it a differing tool is skipped with a warning |
| Overwrite per-tool files | --overwrite (init, import-conflict mode) |
Overwrite config.toml | --overwrite-config (export, init toolbox) |
| Update existing cluster network policies | --update-network-policy (import) |
| Move images to an air-gapped instance | export --with-images, then import --image-path-prefix … |
Toolbox layout
A toolbox is a directory tree. Each tool lives in its own folder with a manifest.toml, a JSON
Thorium config, and (for images) the docker build context. At the root are a config.toml and
the generated toolbox.json:
my-toolbox/
├── config.toml
├── toolbox.json # generated by `build` (and by `export`)
├── images/
│ └── clamav/
│ ├── manifest.toml
│ ├── clamav.json # Thorium image config
│ ├── description.md # image docs (optional); becomes the config's description
│ ├── clamav.tar.gz # only present in --with-images bundles
│ └── Dockerfile
└── pipelines/
└── antivirus/
├── manifest.toml
├── description.md # pipeline docs (optional); becomes the config's description
└── antivirus.json # Thorium pipeline config
Tool docs (description.md)
A tool’s description can live two places: the description field inside its JSON/inline
Thorium config, or a standalone description.md beside its manifest.toml. The Markdown file
exists because authoring a long description as an escaped one-line string inside JSON is
painful — keeping the docs in their own .md file lets you write, diff, and review them as
real Markdown.
Precedence — description.md wins. When toolbox build rolls a tool into toolbox.json,
it reads any description.md next to the manifest, trims trailing whitespace, and writes it
into the config’s description field, replacing whatever the inline config held. If the
inline value was non-empty and differed from the file, the build prints a warning so the
override is visible. If description.md is absent (or present but empty), the inline config’s
description is left untouched. A config that is resolved from a URL at import time
(config_from pointing at a URL) can’t be edited during build, so a description.md beside it
is only warned about, never injected.
toolbox export always writes a description.md for each exported resource — its content is the
resource’s description, or an empty file when the description is unset/empty (never the literal
text null, which would otherwise become the description on re-import). toolbox init scaffolds a
stub. Descriptions surface in the Thorium UI and thorctl images get, so they’re how users — and AI
agents — decide which tool to run.
Toolbox config (config.toml)
Names the toolbox and the registry its images are stored in. The registry may be left empty (for example when exporting a toolbox you intend to push to a registry you don’t know yet).
name = "My Toolbox"
registry = "ghcr.io/org/repo"
# registries = [] # additional registries to tag images in
# image_path_prefix = "" # default registry path prefix for bundled images
# export_image_path = "images" # where `export` places image dirs (relative to the toolbox root)
# export_pipeline_path = "pipelines"
# bundled_images = true # set automatically by `export --with-images`
# toolbox-wide default base-image config (see "Overriding base images" below)
# [base_image]
# image = "ubuntu:22.04"
# image_arg = "IMAGE"
# token = "BASE_REGISTRY_TOKEN" # CI/CD variable NAME (pass-through; unused by build-images)
# user = "BASE_REGISTRY_USER" # CI/CD variable NAME (pass-through)
# allow_override = true
export_image_path / export_pipeline_path set where export writes each tool’s directory
(default images/<name> and pipelines/<name>); set them at scaffold time with
thorctl toolbox init toolbox --image-path … --pipeline-path …. They only affect where export
places files — build discovers manifest.toml files at any depth regardless — and a bundled
image’s tarball travels with its tool directory (each image records that directory in toolbox.json,
so import finds the tarball wherever it was placed).
Image manifest (manifest.toml)
name = "clamav"
type = "image"
config_from = "clamav.json" # local file or a URL
build_path = "./"
image_name = "tools/github.com/cisco-talos/clamav"
version = "latest"
# build = true # set false to reference an already-published image
# exported_image_path = "..." # a build=false image's real registry url (set by export)
# network policy definitions this image references (local files or URLs)
# network_policies_from = ["restricted.policy.json"]
# per-tool base-image config (merged over the config.toml default; per-tool fields win)
# [base_image]
# image = "alpine:3.19"
# image_arg = "IMAGE"
# token = "BASE_REGISTRY_TOKEN" # CI/CD variable NAME (pass-through; unused by build-images)
# user = "BASE_REGISTRY_USER" # CI/CD variable NAME (pass-through)
# allow_override = true
Reusing another image’s container (image_from)
Thorium toolbox images can run the same container image but with different runtime config
(args/env/entrypoint). Instead of giving each its own container, an image can declare image_from
to reuse another toolbox image’s container image by name and version:
name = "clamav-verbose"
type = "image"
config_from = "clamav-verbose.json" # this image's own runtime config (args, etc.)
[image_from]
name = "clamav" # reuse this image's container image
version = "latest" # defaults to "latest" if omitted
image_from is resolved at build time: the referenced image’s container url is written into this
image’s config, so on import both are created as distinct Thorium images sharing one container. Notes:
- It forces
build = false— the image is never built (build_path,[base_image], andexported_image_pathhave no effect whenimage_fromis set). - The reused url takes precedence over any
exported_image_pathorimageurl in the config. - This image’s own config must be local (a
config_fromthat is a URL can’t have the reused url embedded at build time). - The referenced image must be a real image the toolbox knows about (with a container image of its
own). An
image_fromthat points at a missing image, forms a cycle, or targets an image that has no container image failstoolbox build—toolbox.jsonis not written, so the broken image and any pipelines that reference it never reach an instance.
Pipeline manifest (manifest.toml)
name = "antivirus"
type = "pipeline"
description = "Antivirus scanners (licenses may be required)"
version = "latest"
config_from = "antivirus.json"
[images.clamav]
version = "latest"
How container image tags are built
When toolbox build assembles toolbox.json, each image’s container url (its image_tags and the
image field embedded in its config) comes from one of three sources:
-
image_from(highest priority) — the url is taken from the referenced image (see above); no tag is derived for this image. -
Pinned url — for an image with
build = falsethat has noimage_from, the url is used verbatim from its manifest’sexported_image_path(recorded bytoolbox export), or failing that theimageurl already in its config. No derivation happens. -
Derived url — for a buildable image (
build = true), and as the fallback for an unpinnedbuild = falseimage, the tag is derived per registry as:<registry>/[<image_path_prefix>/]<leaf>:<version>[<tag_suffix>]<registry>— fromconfig.toml: the primaryregistryplus anyregistriesextras (blanks and duplicates dropped). One tag is produced per registry; the first is written into the config’simagefield, the rest are mirror push targets. An empty registry list means no tag is derived (the config’s ownimageurl stands instead).<image_path_prefix>— optional path fromconfig.toml, inserted between the registry and the leaf when set.<leaf>— the toolname. Passtoolbox build --use-image-pathto use the manifest’simage_name(a repo-style path) as the leaf instead; with that flag, an image with noimage_nameyields no derived tag.<version>— the manifest’sversion(defaults tolatest; an empty version is rejected).<tag_suffix>— optional, appended to the version viatoolbox build --tag-suffix(e.g.-mybranch) so feature-branch builds get distinct tags.
For example
registry = "ghcr.io/o/r"+ toolname = "strings"+version = "latest"derivesghcr.io/o/r/strings:latest; with--use-image-pathandimage_name = "gnu.org/binutils/strings"it derivesghcr.io/o/r/gnu.org/binutils/strings:latest.
A K8s image that ends up with no container tag from any of these sources is unschedulable, so
toolbox build fails and lists every such image. (Non-K8s scalers — BareMetal, Windows, Kvm,
External — run without a container image and are exempt.)
Creating a toolbox from scratch
Use init to scaffold files (it never overwrites existing files unless you pass --overwrite), then
build to roll them into a toolbox.json:
# scaffold a toolbox with one image and a pipeline that runs it
thorctl toolbox init toolbox -i ./images/clamav -p ./pipelines/antivirus:clamav
# generate toolbox.json (reads ./config.toml by default)
thorctl toolbox build
A toolbox is anchored on its config.toml: build crawls and writes relative to the config.toml’s
directory. Both --path (the crawl root) and --output (the toolbox.json location) default to
that directory, so build from a toolbox root “just works” and thorctl toolbox build -c sub/config.toml builds the toolbox in sub/ (writing sub/toolbox.json). The two are independent
overrides — -o dist/toolbox.json redirects only the artifact, --path ./src redirects only the
crawl — and build prints the resolved crawl root and output path when it runs. (Note: build_path
is recorded relative to --output, so a --output outside the crawled tree produces ../-style
build contexts — fine for import, but a build-images-portable toolbox wants the default
self-contained layout.)
init runs interactively by default, prompting for the key fields and optionally opening your
$EDITOR to review the full config. Pass -n/--non-interactive to accept defaults. You can also
scaffold a single image or pipeline with thorctl toolbox init image ./images/clamav or
thorctl toolbox init pipeline ./pipelines/antivirus -i clamav. The positional path is the
destination; the stub is folded into a toolbox by build’s recursive walk wherever it lands.
init image/init pipeline take an optional -c <config.toml> to validate against an existing
toolbox (a resolution source, not a placement directive — it never moves files). init pipeline
guarantees a dangling-free manifest: every --order image must be declared in --images, and with
-c every referenced image must already exist in the toolbox (else it errors — init never creates
images) and is version-pinned from the toolbox instead of defaulting to latest. init image -c
errors if an image of the same name+version already exists in the toolbox (pass --overwrite to
replace). init toolbox remains the command that creates a new toolbox (config.toml + tools).
Building images locally
Forks without CI can build a toolbox’s container images directly. build-images walks
toolbox.json for image entries with build enabled, builds each entry’s docker context with its
first registry tag, aliases the remaining tags onto the same build, and pushes everything with
--push (requires docker and registry credentials):
thorctl toolbox build-images ./toolbox.json --push # everything buildable
thorctl toolbox build-images ./toolbox.json -i clamav # just one image
Entries with build = false in their manifest or no image_tags are skipped with a note. Once
the images are pushed, toolbox import works as usual. If an image fails to build or push,
build-images reports it, keeps going with the rest, and exits non-zero at the end listing every
image that failed — one broken image doesn’t block the others.
The container runtime is selected from --container-runtime if given, else the container_runtime
setting in your thorctl config, else an autodetected default (docker/podman).
Two flags control how the underlying docker/podman build runs (both apply to every image built in the run):
--no-cache— build without the layer cache (passes--no-cacheto docker/podman), e.g. to pick up a changed remote dependency a cached layer would otherwise mask.--pull— force a fresh pull of referenced images before building (passes--pullto docker/podman). Like docker/podman--pull, this refreshes every image the build references, including theFROMbase.
thorctl toolbox build-images ./toolbox.json --push --no-cache --pull
Base images ([base_image])
Base-image configuration lives in one [base_image] table, available both toolbox-wide (in
config.toml) and per-tool (in manifest.toml). It can override a tool’s Dockerfile base image and
record the credentials an external CI/CD pipeline uses to pull a private base.
# config.toml (global default) or manifest.toml (per-tool)
[base_image]
image = "alpine:3.19" # override the Dockerfile FROM base (optional)
image_arg = "IMAGE" # the build-arg the Dockerfile reads; default "IMAGE"
token = "BASE_REGISTRY_TOKEN" # NAME of a CI/CD variable (pass-through; see below)
user = "BASE_REGISTRY_USER" # NAME of a CI/CD variable (pass-through)
allow_override = true # gates the image/image_arg substitution; default true
Resolution. toolbox build merges the per-tool table over the global one field by field
(per-tool wins) and writes the result onto each image entry in toolbox.json. The
image/image_arg substitution is applied as --build-arg <image_arg>=<image> and is overridden
at build-images time by the --base-image ARG=IMAGE flag, so the effective precedence is CLI >
per-tool > global. Overriding the base requires a parameterized Dockerfile (a build-arg can’t
replace a hardcoded FROM):
ARG IMAGE
FROM ${IMAGE}
allow_override gates the image/image_arg substitution only (default true); set it to
false to keep a tool’s own base. token/user are independent of it.
token/user are opaque pass-through strings — they are intended to hold the names of CI/CD
pipeline variables (do not put real secrets here; toolbox.json is usually committed). thorctl never
resolves or manipulates them, and build-images does not use them (it performs no base-registry
login). If a base image needs auth for build-images, run docker/podman login yourself first.
Defaults / absence. Omitting [base_image] entirely leaves the Dockerfile’s own FROM
untouched (no implicit base). With an image set, image_arg defaults to IMAGE; allow_override
defaults to true; a per-tool field that is omitted inherits the global value. [base_image] is
ignored on image_from images (which are never built).
Exporting from a running instance
export fetches image and pipeline configs from Thorium and writes a complete toolbox directory
(manifests, configs, and a toolbox.json). The real registry URL of each image is preserved
exactly as it is in Thorium.
# export an entire group into a new toolbox
thorctl toolbox export -g static -o ./my-toolbox
# export specific pipelines (their images are pulled in automatically) and standalone images
thorctl toolbox export -p static/antivirus -i static/exiftool -o ./my-toolbox
# append into an existing toolbox by pointing at its config.toml — output defaults to its directory
thorctl toolbox export -c ./my-toolbox/config.toml -p static/newpipeline
The --output/-o directory defaults to the --config directory when --config is given (so
pointing at a toolbox’s config.toml exports into that toolbox), otherwise ./toolbox for a brand-new
toolbox. An explicit -o always wins; the defaulted directory is announced in the output.
Useful flags:
--group-override <group>— rewrite the group of every exported config.--review— open each config in an editor to review/tweak it before writing (off by default, so configs are written as-is).--with-images— also bundle the container image files (see below).--strip-registry— publish a registry-agnostic toolbox: each image config’simageurl is written empty and the manifest’sexported_image_pathis omitted, so a rebuild derives each image path from the toolbox’s ownconfig.tomlregistry/image_path_prefixrather than your pinned url. Images only (pipelines carry no url). Conflicts with--with-images(a bundled import needs the url to tag and push the saved tarball).--overwrite— overwrite existing per-tool files (manifest/JSON/description/policies). It does not touchconfig.toml.--overwrite-config— replace an existingconfig.toml(otherwise it is preserved).
Appending into an existing toolbox is safe by default. config.toml is the toolbox’s identity:
exporting into a directory that already has one preserves it and reuses its settings (announced
in the output) rather than clobbering them, so you don’t need --config. So
thorctl toolbox export -p static/newpipeline -o ./my-toolbox adds the pipeline (and its images)
into ./my-toolbox, and the rebuild folds everything in. Pass --overwrite-config only when you
actually want to change the toolbox’s settings; if a flag like --with-images or --registry
contradicts the preserved config, export warns that the flag is ignored. (--config seeds settings
from another toolbox and anchors the output to that config’s directory unless -o is set — so
-c X/config.toml appends into X, while seeding into a different directory needs an explicit -o.
A --config that points at a missing file isn’t fatal: export warns (“config.toml not found …;
creating a new toolbox there instead of appending”) and creates a new toolbox, so a mistyped or
not-yet-created target shows up as a notice rather than a read error.)
Refreshing a whole toolbox. Running export against an existing toolbox with no
--group/--pipelines/--images and --overwrite re-pulls every tool the toolbox already
contains from Thorium and updates them in place — a one-shot “sync this toolbox to the instance”:
thorctl toolbox export -c ./my-toolbox/config.toml --overwrite # refresh every tool in ./my-toolbox
It enumerates the tool list from the toolbox itself (its toolbox.json, or an on-disk crawl if that’s
gone), updates only tools already present (never adds new ones, never prunes), and warns + leaves
untouched any tool that no longer exists in Thorium. Without --overwrite a no-selection run errors
with a hint (it inherently rewrites the existing configs).
Group as source vs destination, and renames. -g is the source group (what to read from
Thorium); --group-override is the destination group (what to write under in the toolbox),
defaulting to the source group. build identifies an image/pipeline by name + version,
ignoring group, so a tool written under a group that doesn’t match where it already lives in the
toolbox would be a build-breaking duplicate. Export handles this automatically:
# toolbox stores these under a different group than Thorium's "static2"; re-group + update in place:
thorctl toolbox export -g static2 -c ./my-toolbox/config.toml --overwrite
With --overwrite, each existing tool is matched by its name+version and re-grouped in place to
the destination group (the -g group here, or --group-override if you set one) — its files are
rewritten at their existing directory, reported “Re-grouping …”. Without --overwrite, a tool
whose name+version already exists under a different group is skipped with a warning (suggesting
--overwrite) rather than written as a duplicate. (A same name at a different version under
another group is warned but allowed, since build keeps distinct versions.) Refresh-all (no selection)
still fetches by the toolbox’s own group, so use -g <thorium-group> --overwrite to bridge a rename.
When appending into an existing toolbox, export reconciles against it. The existing toolbox is read
from its committed toolbox.json; if that file is missing or unparsable, export falls back to crawling
the on-disk tool manifests, so a deleted or stale toolbox.json doesn’t make the append re-write what’s
already there. Each image and pipeline records its on-disk directory in toolbox.json, so a re-export
updates a tool where it already lives instead of writing a duplicate at the default layout. Per
matched tool (group/name):
- unchanged (byte-identical config) → a no-op, and the container re-bundle is skipped when the tarball is already saved (reported “Unchanged”);
- differs, with
--overwrite→ updated in place. For an image this refreshes only the Thorium config (<name>.json), description, and policy defs and keeps itsmanifest.toml(so hand-setbuild/build_path/[base_image]/image_fromsurvive); a pipeline manifest is regenerated; - differs, without
--overwrite→ skipped with a warning suggesting--overwrite, and the rest of the export continues; - an explicit
=dirpointing somewhere other than where the tool already lives → skipped (a second copy would failbuild); omit=dirto update in place.
A fresh export (no existing toolbox) does none of this. Note: an image update preserves the manifest,
so a change to the image’s network-policy set in Thorium won’t re-link network_policies_from — use
=dir (a full write) or remove + re-export to pick that up.
Placing a single resource (=dir). A -i/-p entry may carry a group/name=dir suffix to
write that resource’s files into a chosen directory instead of the configured/default layout — for
example to fold a Thorium image config into a directory that already holds its Dockerfile:
thorctl toolbox export -i static/clamav=tools/clamav -o ./my-toolbox
# writes tools/clamav/{manifest.toml, clamav.json, description.md, *.policy.json}
dir may be relative or absolute. A relative dir is interpreted against the toolbox root; an
absolute one is taken literally; either is normalized and re-expressed relative to the root — so an
explicit path into a toolbox elsewhere works, e.g. with -c ../../other/tb/config.toml you can write
-i static/clamav=../../other/tb/tools/clamav (it lands at tools/clamav inside that toolbox). The
only rule is that it must resolve inside the toolbox root; a dir that lands outside is rejected
(build only includes files under the toolbox).
=dir is placement only — it never changes which pipeline or images are selected (a pipeline’s
membership and order come from Thorium). Placement precedence per resource is: explicit =dir → the
directory the tool already occupies in the toolbox (so re-exports update in place) → the
configured/default layout. Whole-group exports and auto-pulled dependency images aren’t named, so they
reuse their existing directory or the configured/default layout unless an image is also named with its
own =dir.
If the =dir destination already contains a Dockerfile, export reads it as a build context you are
folding the config into and writes that image’s manifest.toml with build = true (and no
exported_image_path), so a later build builds the image from that context rather than referencing
the original registry url:
# tools/clamav/ already holds a Dockerfile
thorctl toolbox export -i static/clamav=tools/clamav -o ./my-toolbox
# clamav.json + manifest.toml (build = true) land beside the existing Dockerfile
This auto-detect applies only to the explicit-=dir case (a default-layout dir is created fresh and
never holds a Dockerfile); the default build = false reference-only manifest is used everywhere
else. The detection is announced with a log line.
Importing into an instance
import reads a toolbox.json from a local path or a URL and creates the images, pipelines, and
any missing groups in the target Thorium instance.
thorctl toolbox import ./my-toolbox/toolbox.json
thorctl toolbox import https://raw.githubusercontent.com/cisagov/thorium/refs/heads/main/tools/toolbox.json
If an image or pipeline already exists, the conflict is handled according to one of three modes (new resources are created in all of them):
| Mode | Flag | Behavior for existing resources |
|---|---|---|
| Interactive (default) | — | Prompts per changed resource: Edit (resolve a git-style diff in your $EDITOR), Skip, Apply (accept incoming), or Quit |
| Overwrite | --overwrite | Applies every incoming change without prompting |
| Skip conflicts | --skip-conflicts | Never touches existing resources; logs a warning per skipped resource listing the fields that differ |
--overwrite and --skip-conflicts are mutually exclusive. The upfront confirmation screen
(shown before an interactive merge or before creating new groups/network policies) only appears
in the default interactive mode on a real terminal; passing --overwrite or --skip-conflicts,
or running without a TTY (CI, pipes), skips it. --skip-conflicts is therefore the fully
unattended form for CI and agents that must never overwrite local changes.
--group-override <group> forces everything into a single group.
Diffing a toolbox against an instance
diff shows what an import of a toolbox would change, rendered like git diff. It accepts a
toolbox.json (path or URL) or a toolbox repo directory — directories are built in-memory from
their manifests, so the diff always reflects the configs as they sit on disk:
# compare a repo checkout against the instance it was imported into
thorctl toolbox diff ./my-toolbox --group-override sandbox
# gate CI on drift (exit 1 when anything differs, like git diff --exit-code)
thorctl toolbox diff ./my-toolbox/toolbox.json --exit-code
diff runs the toolbox through the same pre-processing as import before comparing, so what
it shows is what an import would actually apply: structural validation, group capture, any
--group-override, group-coherence validation, and collision resolution (the automatic renames
import performs when two toolbox resources would collide). Resources dropped by validation are
warned about and excluded from the diff, exactly as import would drop them.
Changed resources show unified hunks over their normalized configs (server-only fields like
creators and bans never appear as drift). A resource present on only one side is reported on a
single line rather than a full-body add/delete: resources only in the toolbox as
only in toolbox/<group>/<name> (…) — not in <host[:port]>, and resources in the toolbox’s groups
that the toolbox doesn’t name as only in <host[:port]>/<group>/<name> (…) — not in this toolbox.
The instance is named by its host (and explicit port, if any), and changed resources use that same
<host[:port]>/<group>/<name> header on their instance side. Bundled network policies are included
the same way — toolbox-only ones as a single line, mismatched ones as notes (imports never update
policies). A trailing summary counts changed / only in toolbox / only in <host[:port]> / unchanged.
--exit-code reflects only what an import would change. It exits 1 when there is an
actionable difference — a changed resource, a toolbox-only resource that import would create, or a
network policy that differs — and 0 otherwise. Resources that exist only on the instance are
informational (import never deletes), so they do not trip a non-zero exit on their own.
Network policies
Images can reference Thorium network policies by name, and a toolbox carries the policy definitions so those references stay valid on import:
toolbox exportwrites each referenced policy to<image dir>/<name>.policy.jsonand records it in the image’smanifest.tomlundernetwork_policies_from(the same policy referenced by several images is written to each — identical duplicates dedupe on import). Each exported policy’sgroupsare scoped to the group of the image that references it (and rewritten by--group-override), so a bundled policy never carries groups the toolbox isn’t exporting.toolbox buildbundles those files intotoolbox.json; URL entries are fetched at import time likeconfig_from. A toolbox carrying two different definitions under one policy name fails validation before anything is applied.toolbox importcreates missing policies (with--group-overrideapplied to their groups) before any images. A policy that already exists in the target is never updated — if it doesn’t 100% match the bundled definition, a warning names the differing fields and the target’s policy is left alone. Created policies are part of the rollback journal.
Removing a toolbox
remove deletes the pipelines and images named by a toolbox manifest from the target instance —
pipelines first, then images. Resources that don’t exist are reported and skipped; groups are
never deleted. Pass --group-override if the toolbox was imported with one:
thorctl toolbox remove ./my-toolbox/toolbox.json --group-override sandbox
Single resources can be deleted directly with thorctl images delete <name...> -g <group> and
thorctl pipelines delete <name...> -g <group> (both confirm first; -y to skip).
Offline transfer with bundled images
To move a whole toolbox — including the container images — to an air-gapped Thorium instance, bundle the image files on a connected host and push them into the offline registry on import. You do not need to know the offline registry when exporting.
1. On a connected host, export with --with-images. Each image is docker pulled and saved to
images/<name>/<name>.tar.gz, and the toolbox is marked as bundled:
thorctl toolbox export -g static --with-images -o ./offline-toolbox
Bundling is best-effort: an image that can’t be pulled or saved is warned about and skipped, and the
rest of the toolbox is still written (the skipped image simply ships no tarball and keeps its
original registry URL). Because that leaves the bundle incomplete, export then prints an
INCOMPLETE summary and exits non-zero — so a scripted export → import handoff doesn’t treat a
toolbox with missing tarballs (or an omitted, dangling network policy) as a complete one. A clean
export exits 0.
2. Move the ./offline-toolbox directory to the offline environment (it is fully
self-contained).
3. On the offline host, import and provide the target registry base path with
--image-path-prefix. Each bundled image is loaded, retagged to
<image-path-prefix>/<group>/<name>:<tag>, pushed, and the imported image’s config is rewritten to
point at the offline registry:
thorctl toolbox import ./offline-toolbox/toolbox.json \
--image-path-prefix registry.offline.local:5000/team
# clamav (group "static") -> registry.offline.local:5000/team/static/clamav:latest
If you omit --image-path-prefix for a bundled toolbox, the prefix recorded in the manifest is
used; otherwise you are prompted for one — and if the session can’t prompt (--overwrite,
--skip-conflicts, or no TTY), the import errors instead. The on-disk bundle always keeps the
original image URLs, so it remains re-importable to additional environments. Bundled imports
require docker and must be run from a local path (not a URL).
Pushing the bundled images is best-effort: if an image can’t be loaded, retagged, or pushed, the import warns and continues. The image’s config is still created in Thorium pointing at the offline registry, so once you push that image manually (or re-run the import) the resource works — a single unpushable image doesn’t block the rest of the import. A run that finished with any failed pushes exits non-zero and lists them.
Note:
--with-images/--image-path-prefixneed a container runtime (docker/podman) on the host to actually move images. There is no up-front capability check: if no runtime is available, every image is simply skipped (warned) and the export exits non-zero as incomplete, rather than failing fast. A toolbox exported without--with-imageskeeps each image’s original registry URL, so importing it simply points Thorium at the existing registry — no image transport happens.
Note: KVM-scaled images round-trip their
kvmsettings, but the underlying VM disk image is not part of any export — Thorium has no transport for VM disks. An imported KVM image is creatable but will not run until its disk is moved to the target environment out of band.
Toolbox vs images/pipelines export
thorctl images export|import and thorctl pipelines export|import move a single image or
pipeline (with its container files) between instances and are ideal for one-off transfers and
fine-grained registry control (--registry, --registry-override, --migrate-registry). Reach
for a toolbox when you want to package many images and pipelines together as one portable,
versioned unit — to publish a curated set of tools, share them via a URL, or move an entire
collection (optionally with images) to another instance.
The plain import commands use the same three conflict modes as toolbox import (interactive
merge by default, --overwrite, --skip-conflicts), show the same confirmation summary (shown
only on an interactive terminal; --skip-conflicts or a non-TTY session bypasses it), and
auto-create the target group if it doesn’t exist.
pipelines import imports the images referenced by each pipeline’s order first, then the
pipeline itself, all under one rollback journal: if the import stops early (an error, or Quit
in the merge editor), you are offered a rollback of everything applied so far — or pass
--rollback-on-failure for non-interactive runs. Rollback only undoes Thorium state; registry
pushes are left in place.