Build
build
¶
Builds L0 (base dev) and L1 (agent CLI) container images via podman.
Owns the L0 (base dev) and L1 (agent CLI) Dockerfile templates, resource
staging, image naming, and podman build invocation.
Image layer architecture::
L0 (base) — Distro + dev tools + init script + dev user
L1 (agent) — All AI agent CLIs, shell environment, ACP wrappers
L1 is self-sufficient for standalone use — all user
config (repo URL, SSH, branch, gate) is runtime.
─── boundary: above owned by terok-executor, below by terok ───
L2 (project)— Optional: user Dockerfile snippet (custom packages)
Only built when project has docker snippet config.
terok-executor run claude . launches directly on the L1 image — no L2
build needed. terok adds L2 only for project-specific image customisation.
Usage as a library::
from terok_executor import build_base_images
images = build_base_images("fedora:44")
# images.l0 = "terok-l0:fedora-44"
# images.l1 = "terok-l1-cli:fedora-44"
The L0/L1 templates select between Debian/Ubuntu (apt) and Fedora-like
(dnf) package managers via a family Jinja2 variable resolved by
detect_family from the base image name (or an explicit override).
L1 is roster-driven: each agent's install steps live in its YAML file
(install.run_as_root / install.run_as_dev), and the L1 template
loops over the resolved selection. Build emits an OCI label
ai.terok.agents=<csv>, an in-container manifest
/etc/terok/installed.env, and pre-rendered hilfe help fragments —
all derived from the same selection.
DEFAULT_BASE_IMAGE = 'fedora:44'
module-attribute
¶
Default base OS image when none is specified.
AGENTS_LABEL = 'ai.terok.agents'
module-attribute
¶
OCI label naming the roster entries baked into an L1 image.
INSTALLED_ENV_PATH = '/etc/terok/installed.env'
module-attribute
¶
In-container env file that scripts source to learn what's installed.
BuildError
¶
Bases: RuntimeError
Raised when base-image construction cannot complete.
The CLI maps this to a user-facing error message; library callers
can catch it without being terminated by SystemExit.
ImageSet(l0, l1, l1_sidecar=None)
dataclass
¶
L0 + L1 image tags produced by a build.
ImageBuilder(base_image=DEFAULT_BASE_IMAGE, family=None)
dataclass
¶
Build pipeline for terok agent container images.
Holds the (base_image, family) the L0/L1/L2 build stack is
anchored on. Build operations are instance-bound; pure
family detection, image introspection, and resource staging stay
as static methods — they don't depend on the builder's state.
Two scopes of operations:
- Instance methods — apply
self.base_imageandself.familyto a podman build (build_base,build_sidecar,ensure_default_l1), tag computations (l0_tag,l1_tag,l1_sidecar_tag), and Dockerfile rendering (render_l0,render_l1,render_l1_sidecar). - Static methods — pure helpers that operate on arbitrary
inputs:
detect_family,image_agents,stage_scripts,stage_tmux_config,stage_toad_agents.
base_image = DEFAULT_BASE_IMAGE
class-attribute
instance-attribute
¶
Base OS image the L0 layer FROMs (e.g. fedora:44).
family = None
class-attribute
instance-attribute
¶
Package family override ("deb" / "rpm") — auto-detected when None.
l0_tag
property
¶
L0 image tag for self.base_image.
l1_sidecar_tag
property
¶
L1 sidecar image tag for self.base_image.
build_base(*, agents='all', rebuild=False, full_rebuild=False, build_dir=None, tag_as_default=False)
¶
Build L0 + L1 images for agents; returns the resulting tag pair.
See module-level build_base_images for the parameter contract.
Source code in src/terok_executor/container/build.py
build_sidecar(*, tool_name='coderabbit', rebuild=False, full_rebuild=False, build_dir=None)
¶
Build the L1 sidecar image variant for tool_name; returns the tag.
Source code in src/terok_executor/container/build.py
ensure_default_l1(agents='all')
¶
Return the default-alias L1 tag, building the default L1 if absent.
l1_tag(agents=None)
¶
L1 image tag for agents under self.base_image (alias when None).
render_l0()
¶
Render the L0 Dockerfile for this base.
Instance-bound because L0 is anchored on self.base_image.
render_l1(l0_tag, *, family, agents='all', cache_bust='0')
staticmethod
¶
Render the L1 CLI Dockerfile for agents on top of l0_tag.
Static because the L1 stage depends only on the L0 tag and the
package family — it never touches self.base_image. Callers
that already have an ImageBuilder
can pass builder._family to thread the resolved family through.
Source code in src/terok_executor/container/build.py
render_l1_sidecar(l0_tag, *, family, tool_name='coderabbit', cache_bust='0')
staticmethod
¶
Render the L1 sidecar Dockerfile for tool_name on top of l0_tag.
Static for the same reason as render_l1.
Source code in src/terok_executor/container/build.py
detect_family(base_image, override=None)
staticmethod
¶
Resolve the package family ("deb" / "rpm") for base_image.
image_agents(image)
staticmethod
¶
stage_scripts(dest)
staticmethod
¶
stage_tmux_config(dest)
staticmethod
¶
detect_family(base_image, override=None)
¶
Resolve the package family (deb or rpm) for base_image.
override — when set, must be "deb" or "rpm" and wins over
detection (used to support unknown bases via project config).
Detection matches a small allowlist of known image prefixes
(Ubuntu/Debian, Fedora, the official Podman container, NVIDIA CUDA/HPC
SDK). NVIDIA images are inspected at the tag level so UBI variants
(e.g. …:13.0.0-devel-ubi9) resolve to rpm while Ubuntu
variants resolve to deb. Unknown images raise BuildError
with a hint to set family: explicitly.
Source code in src/terok_executor/container/build.py
build_project_image(*, dockerfile, context_dir, target_tag, extra_tags=(), build_args=None, labels=None, no_cache=False, pull_always=False)
¶
Build an OCI image from a pre-rendered Dockerfile.
The thin podman build invoker that the three opinionated factories
in this module (build_base_images, build_sidecar_image,
and terok's project/L2 build) share. Callers own Dockerfile
rendering, tag naming, label computation, and build-context staging —
this function only assembles flags and shells out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dockerfile
|
Path
|
Path to the pre-rendered Dockerfile ( |
required |
context_dir
|
Path
|
Build context directory (final positional argument). |
required |
target_tag
|
str
|
Primary image tag ( |
required |
extra_tags
|
tuple[str, ...]
|
Additional tags applied to the same build (each becomes
another |
()
|
build_args
|
dict[str, str] | None
|
|
None
|
labels
|
dict[str, str] | None
|
|
None
|
no_cache
|
bool
|
Force full rebuild. |
False
|
pull_always
|
bool
|
Pull the base image even if a local copy exists. |
False
|
Raises:
| Type | Description |
|---|---|
BuildError
|
When podman is not on PATH or the build exits non-zero. |
Source code in src/terok_executor/container/build.py
build_base_images(base_image=DEFAULT_BASE_IMAGE, *, family=None, agents='all', rebuild=False, full_rebuild=False, build_dir=None, tag_as_default=False)
¶
Build L0 + L1 container images and return their tags.
Skips building if images already exist locally (unless rebuild or full_rebuild is set). Uses a temporary directory for the build context by default; pass build_dir to use a specific (empty or non-existent) directory instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_image
|
str
|
Base OS image (e.g. |
DEFAULT_BASE_IMAGE
|
family
|
str | None
|
Override for the package family ( |
None
|
agents
|
str | tuple[str, ...]
|
Roster entries to install, as the literal string |
'all'
|
rebuild
|
bool
|
Force rebuild with cache bust (refreshes agent installs). |
False
|
full_rebuild
|
bool
|
Force rebuild with |
False
|
build_dir
|
Path | None
|
Build context directory (must be empty or absent). |
None
|
tag_as_default
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
ImageSet
|
|
Raises:
| Type | Description |
|---|---|
BuildError
|
If podman is missing, the family cannot be resolved, or a build step fails. |
ValueError
|
If build_dir is a file or a non-empty directory, or if agents contains unknown roster entries. |
Source code in src/terok_executor/container/build.py
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 | |
build_sidecar_image(base_image=DEFAULT_BASE_IMAGE, *, family=None, tool_name='coderabbit', rebuild=False, full_rebuild=False, build_dir=None)
¶
Build the L1 sidecar image for a specific tool. Returns the image tag.
Ensures L0 exists first (builds it if missing), then builds the sidecar image FROM L0. The sidecar contains only the named tool — no agent CLIs, no LLMs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_image
|
str
|
Base OS image (passed through to L0 build). |
DEFAULT_BASE_IMAGE
|
family
|
str | None
|
Override for the package family ( |
None
|
tool_name
|
str
|
Tool to install (selects Jinja2 conditional in template). |
'coderabbit'
|
rebuild
|
bool
|
Force rebuild with cache bust. |
False
|
full_rebuild
|
bool
|
Force rebuild with |
False
|
build_dir
|
Path | None
|
Build context directory (must be empty or absent). |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The sidecar image tag (e.g. |
Raises:
| Type | Description |
|---|---|
BuildError
|
If podman is missing, the family cannot be resolved, or a build step fails. |
ValueError
|
If build_dir is a file or a non-empty directory. |
Source code in src/terok_executor/container/build.py
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 | |
prepare_build_context(dest)
¶
Stage auxiliary resources into a build context directory.
After calling this, dest contains the resources that Dockerfile
COPY directives reference:
scripts/— container helper scripts (init, env, ACP wrappers)toad-agents/— ACP agent TOML definitionstmux/— container tmux config
Dockerfiles themselves are not written here — they are rendered
and placed by build_base_images (which calls this function
internally).
Source code in src/terok_executor/container/build.py
render_l0(base_image=DEFAULT_BASE_IMAGE, *, family=None)
¶
Render the L0 (base dev) Dockerfile.
The base_image is normalised before rendering so that blank or
whitespace-only values produce a valid Dockerfile. family
("deb" or "rpm") selects the package-manager branch of the
template; None resolves it via detect_family.
The rendered template ships a single vendor unit
(sshd-terok.service) gated on a stock ConditionFileNotEmpty=
over /etc/ssh/authorized_keys.d/terok. The trust file ships
empty, so the unit is skipped at boot under crun (no keys ⇒ no
listener); under krun terok bind-mounts the live host pubkey over
it at launch, the condition flips to true, sshd starts on TCP 22,
and the host reaches the guest via the per-task host port that
podman's passt has forwarded into the guest namespace. One image,
one build, two runtimes — and "off" is structural, not just inert.
Source code in src/terok_executor/container/build.py
render_l1(l0_image, *, family, agents='all', cache_bust='0')
¶
Render the L1 (agent CLI) Dockerfile for the given agent selection.
l0_image is the tag of the L0 image to build on top of. family
("deb" or "rpm") selects the package-manager branch and is
required — there is no L0 reference to detect from at this point, so
callers must supply the value resolved at the L0 level (typically via
detect_family). Each roster install snippet is itself rendered
as a Jinja template with family in scope, so snippets can carry
{% if family == "deb" %}…{% else %}…{% endif %} branches for
package-manager-specific commands. agents is a tuple of
already-resolved roster names (or the literal string "all"); the
template loops over them and emits each one's install snippets.
cache_bust invalidates the per-agent install layers when changed
(typically set to a Unix timestamp).
Source code in src/terok_executor/container/build.py
render_l1_sidecar(l0_image, *, family, tool_name='coderabbit', cache_bust='0')
¶
Render the L1 sidecar (tool-only) Dockerfile.
The sidecar image is built FROM L0 (not L1) and installs a single tool binary — no agent CLIs, no LLMs. family (required) selects the package-manager branch; tool_name selects which tool install block to activate via Jinja2 conditional.
Source code in src/terok_executor/container/build.py
stage_scripts(dest)
¶
Stage container helper scripts into dest.
Copies executor's own resources/scripts/ then overlays the
socat-based bridge scripts that ship with terok_sandbox
(ensure-bridges.sh + ssh-agent-bridge.sh). The bridges
live in sandbox because they encode sandbox-level concerns with
no executor-specific logic; executor still bundles them into the
container image so the Dockerfile's COPY scripts/… lines keep
finding them at their established names.
Raises BuildError
when terok_sandbox is not importable — sandbox is a hard
dependency in pyproject.toml, but a broken install would
otherwise surface as a raw ModuleNotFoundError traceback.
Source code in src/terok_executor/container/build.py
stage_toad_agents(dest)
¶
Stage Toad ACP agent TOML definitions into dest.
These describe OpenCode-based agents (Blablador, KISSKI, etc.) that are injected into Toad's bundled agent directory at container build time.
Source code in src/terok_executor/container/build.py
stage_help_fragments(dest, agents)
¶
Render per-section hilfe help fragments into dest.
Writes one file per section (currently agent and dev_tool)
containing the labels of the selected agents that have a help:
section, with backslash escapes (\033[...]) interpreted into
real ANSI sequences so that hilfe only needs to cat them.
Empty sections are omitted entirely; hilfe skips missing files.
Source code in src/terok_executor/container/build.py
stage_tmux_config(dest)
¶
Stage the container tmux configuration into dest.
Copies container-tmux.conf — the green-status-bar config that
distinguishes container tmux sessions from host tmux.
Source code in src/terok_executor/container/build.py
l0_image_tag(base_image)
¶
l1_image_tag(base_image, agents=None)
¶
Return the L1 agent CLI image tag for base_image and a selection.
When agents is None, returns the unsuffixed default-alias
(e.g. terok-l1-cli:fedora-44). This alias points at whichever
L1 was last built with tag_as_default=True — i.e. the L1 that
holds the user's configured default agent selection. Project /
per-agent / partial builds get only their suffixed tag and never
touch the alias, so terok auth <provider> can rely on the alias
actually containing every agent the user configured.
When agents is a tuple of names, appends a sorted -a-b-c
suffix (- is the only spec-valid separator that _base_tag
already uses) so multiple selections coexist in the local image
store and stay individually addressable. Agent name fragments are
passed through the same _base_tag sanitiser to keep the final
tag within the OCI tag charset ([A-Za-z0-9_.-]).
The full tag (after :) is bounded by _MAX_TAG_LEN. When
the readable base-a-b-c form would overflow, the agent portion
is replaced with a SHA1 digest of the sorted selection — same
collision-resistant fallback pattern _base_tag uses
internally for overlong image names.
Source code in src/terok_executor/container/build.py
l1_sidecar_image_tag(base_image)
¶
image_agents(image)
¶
Return the set of agent names installed in image.
Reads the ai.terok.agents OCI label baked into the L1 image at
build time (see AGENTS_LABEL).
Returns an empty set if the image is missing, has no label, or its
label is empty — never raises on inspection failure, since callers
use the result to make a "image good enough?" decision and an
empty set always means "no, rebuild".
Source code in src/terok_executor/container/build.py
ensure_default_l1(base_image=DEFAULT_BASE_IMAGE, *, family=None, agents='all')
¶
Return the default-alias L1 tag, building the user's default L1 if absent.
Used by terok auth (and the equivalent standalone-executor flow)
to resolve a host-wide L1 image that contains every agent the user
has configured. If the alias already exists locally, it is trusted
and returned as-is — the alias is reserved for the user's default
selection (see l1_image_tag),
so its contents are well-defined. When the alias is missing the
function builds it via build_base_images
with tag_as_default=True.
agents defaults to the literal string "all" so standalone
callers get the whole roster. terok passes the user's configured
image.agents value here so the alias means "every agent the
user has enabled" rather than the implementation-default roster.