Terok executor
terok_executor
¶
terok-executor: single-agent task runner for hardened Podman containers.
Builds agent images, launches instrumented containers, and manages the
lifecycle of one AI coding agent at a time. Designed for standalone use
(terok-executor run claude .) and as a library for terok orchestration.
The public surface is __all__ below. Key entry points:
AgentRunner— launch agents in containersAuthenticator— credential flowImageBuilder— image constructionAgentRoster.shared— YAML agent registry (process-wide cache)
Implementation-detail types (raw config schema fragments, ACP error
classes, internal result types, sidecar image / inject helpers) stay
in their submodules; reach into terok_executor.<sub> when you
need them.
__version__ = _meta_version('terok-executor')
module-attribute
¶
COMMANDS = CommandTree(OWN_COMMANDS + (CommandDef(name='sandbox', help='Sandbox subsystem (full deep tree — same verbs as terok-sandbox)', children=(SANDBOX_TREE.roots)),) + VAULT_COMMANDS)
module-attribute
¶
AGENT_COMMANDS = (RUN_COMMAND, RUN_TOOL_COMMAND, AUTH_COMMAND, AGENTS_COMMAND, BUILD_COMMAND, SETUP_COMMAND, UNINSTALL_COMMAND, LIST_COMMAND, STOP_COMMAND, SHOW_CONFIG_COMMAND, ACP_COMMAND)
module-attribute
¶
AGENTS_LABEL = 'ai.terok.agents'
module-attribute
¶
OCI label naming the roster entries baked into an L1 image.
DEFAULT_BASE_IMAGE = 'fedora:44'
module-attribute
¶
Default base OS image when none is specified.
AUTH_PROVIDERS = {}
module-attribute
¶
All known auth providers (agents + tools), keyed by name. Loaded from resources/agents/*.yaml.
VAULT_COMMANDS = (SANDBOX_TREE.find_at(('vault',)),)
module-attribute
¶
AGENT_PROVIDERS = {}
module-attribute
¶
All agent providers, keyed by name. Loaded from resources/agents/*.yaml.
PROVIDER_NAMES = ()
module-attribute
¶
__all__ = ['__version__', 'ACPEndpointStatus', 'acp_socket_is_live', 'list_authenticated_agents', 'AGENT_PROVIDERS', 'AgentProvider', 'CLIOverrides', 'PROVIDER_NAMES', 'get_provider', 'resolve_provider_value', 'AgentConfigSpec', 'parse_md_agent', 'prepare_agent_config_dir', 'AUTH_PROVIDERS', 'Authenticator', 'AuthSession', 'prepare_oauth_session', 'store_api_key', 'bundled_default_instructions', 'resolve_instructions', 'ConfigScope', 'ConfigStack', 'ExecutorConfigView', 'RawImageSection', 'AGENTS_LABEL', 'DEFAULT_BASE_IMAGE', 'BuildError', 'ImageBuilder', 'ImageSet', 'build_project_image', 'scan_leaked_credentials', 'AgentRoster', 'AGENT_COMMANDS', 'COMMANDS', 'VAULT_COMMANDS', 'SharedMountStorageInfo', 'TaskStorageInfo', 'AgentRunner', 'ContainerEnvSpec', 'assemble_container_env', 'inject_prompt', 'seed_workspace_from_clone_cache', 'ensure_sandbox_ready', 'KrunHost', 'KrunHostKeypair', 'ensure_krun_host_keypair']
module-attribute
¶
ACPEndpointStatus
¶
Bases: StrEnum
Live state of a per-task ACP endpoint.
The host classifier (Project.acp_endpoints) attaches one of
these to every running task; the value drives both the rendered
row in acp list and the decision acp connect makes about
whether to spawn a daemon.
ACTIVE = 'active'
class-attribute
instance-attribute
¶
Daemon up, socket bound, ready for client connections.
READY = 'ready'
class-attribute
instance-attribute
¶
Task running with at least one authenticated agent — a daemon
will spawn on first terok acp connect.
UNSUPPORTED = 'unsupported'
class-attribute
instance-attribute
¶
Task running but no in-image agents are authenticated. Connect would fail; surface honestly so the user knows to authenticate.
ExecutorConfigView
¶
Bases: SandboxConfigView
The slice of config.yml executor owns + sandbox owns (transitively).
Inherits all eight sandbox-owned sections from
SandboxConfigView and adds
the executor-owned image: section. extra="allow" keeps the
view tolerant of foreign top-level keys (terok's tui: /
logs: / tasks: / git: / hooks:) — standalone
terok-executor run flows don't crash on a complete ecosystem
config, no need to vendor a list of terok's section names here.
terok's RawGlobalConfig inherits from this class and flips
back to extra="forbid": the topmost layer knows every section,
so a typo at the top level is caught there.
The class also exposes staticmethods for reading and writing the
image: section on disk: image_agents(),
image_base_image(), and set_image_agents(selection). The
schema thus owns both the shape and the canonical accessors
for its owned section, rather than scattering one helper per
operation across a separate config module.
model_config = ConfigDict(extra='allow')
class-attribute
instance-attribute
¶
image = Field(default_factory=RawImageSection)
class-attribute
instance-attribute
¶
image_agents()
staticmethod
¶
Return the effective image.agents, or None when unset.
None distinguishes "field absent" from "all" (the
explicit "every roster entry" selector).
Source code in src/terok_executor/config_schema.py
image_base_image()
staticmethod
¶
Return the explicit image.base_image, or None when unset.
Callers apply the schema fallback themselves
(DEFAULT_BASE_IMAGE) —
keeping that constant out of this module preserves the
foundation-layer boundary (schema sits below container/build).
Source code in src/terok_executor/config_schema.py
set_image_agents(selection)
staticmethod
¶
Write selection into image.agents and return the file path.
Caller validates selection up-front (typically via
AgentRoster.validate_selection).
Invalidates terok-util's process-wide read_config_section
cache before returning so the next image_agents() /
image_base_image() call observes the freshly-written value
rather than the in-memory snapshot taken before the write.
Source code in src/terok_executor/config_schema.py
RawImageSection
¶
Bases: BaseModel
The image: section — base image, agent roster, Dockerfile snippets.
Strict on its own keys (extra="forbid"). Same shape used in both
the global config.yml (defaults across projects) and per-project
project.yml (project overrides).
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
¶
base_image = Field(default='fedora:44', description='Base container image for builds')
class-attribute
instance-attribute
¶
family = Field(default=None, description='Package family for the L0/L1 build (``deb`` or ``rpm``). Leave unset to auto-detect from *base_image*; set explicitly when the image is outside the known allowlist.')
class-attribute
instance-attribute
¶
agents = Field(default=None, description='Comma-separated roster entries to install in L1, or "all". Prefix a name with "-" to exclude it from the selection (e.g. "all,-vibe" or just "-vibe" — both mean "everything except vibe"). Inherits from the global config when unset.')
class-attribute
instance-attribute
¶
user_snippet_inline = Field(default=None, description='Inline Dockerfile snippet injected into the project image')
class-attribute
instance-attribute
¶
user_snippet_file = Field(default=None, description='Path to a file containing a Dockerfile snippet')
class-attribute
instance-attribute
¶
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.
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
¶
ImageSet(l0, l1, l1_sidecar=None)
dataclass
¶
L0 + L1 image tags produced by a build.
ContainerEnvSpec(task_id, provider_name, workspace_host_path, code_repo=None, clone_from=None, branch=None, git_author_name=None, git_author_email=None, git_committer_name=None, git_committer_email=None, authorship='agent', human_name='Nobody', human_email='nobody@localhost', credential_scope='standalone', credential_set='default', vault_transport='direct', vault_required=False, scan_leaked_creds=False, enabled_vault_patch_providers=None, disabled_vault_patch_providers=None, expose_credential_providers=frozenset(), unrestricted=True, timezone=None, agent_config_dir=None, shared_dir=None, shared_mount='/shared', task_dir=None, envs_dir=None, extra_volumes=())
dataclass
¶
Specification for container environment assembly.
All fields use primitives or Path — no terok-specific
types. Callers pre-resolve domain-specific decisions (security class,
authorship mode, SSH mount, gate mirror creation) and pass results here.
task_id
instance-attribute
¶
Unique task identifier.
provider_name
instance-attribute
¶
Agent provider name (e.g. "claude", "codex").
workspace_host_path
instance-attribute
¶
Host-side workspace directory — caller pre-creates, mounted as /workspace:Z.
code_repo = None
class-attribute
instance-attribute
¶
Git URL to clone inside the container (→ CODE_REPO).
clone_from = None
class-attribute
instance-attribute
¶
Secondary clone source for online-mode gate optimization (→ CLONE_FROM).
branch = None
class-attribute
instance-attribute
¶
Git branch to check out (→ GIT_BRANCH).
git_author_name = None
class-attribute
instance-attribute
¶
Resolved from roster provider if None.
git_author_email = None
class-attribute
instance-attribute
¶
git_committer_name = None
class-attribute
instance-attribute
¶
git_committer_email = None
class-attribute
instance-attribute
¶
authorship = 'agent'
class-attribute
instance-attribute
¶
Authorship mode consumed by in-container wrappers (→ TEROK_GIT_AUTHORSHIP).
human_name = 'Nobody'
class-attribute
instance-attribute
¶
Human operator name (→ HUMAN_GIT_NAME). terok resolves from project
config / git config; standalone uses the default or --git-identity-from-host.
human_email = 'nobody@localhost'
class-attribute
instance-attribute
¶
Human operator email (→ HUMAN_GIT_EMAIL).
credential_scope = 'standalone'
class-attribute
instance-attribute
¶
Scope for vault token creation. terok passes project.id.
credential_set = 'default'
class-attribute
instance-attribute
¶
Vault storage namespace to read credentials from. Pairs with
Authenticator.run's
credential_set — if the auth flow stored a token under set
foo, the runtime must read from set foo too or the container
will see empty env. Default "default" matches the shared
host-wide bucket every standalone caller uses; terok overrides for
per-project credentials.
vault_transport = 'direct'
class-attribute
instance-attribute
¶
Vault transport mode: "direct" (HTTP base URL) or "socket"
(Unix socket path via socket_env).
vault_required = False
class-attribute
instance-attribute
¶
When True, raise SystemExit if the vault is
unreachable. When False (default), soft-fail to empty env.
scan_leaked_creds = False
class-attribute
instance-attribute
¶
When True, scan shared mounts for real credential files and emit
warnings. Standalone mode defaults to off; terok enables this.
enabled_vault_patch_providers = None
class-attribute
instance-attribute
¶
Provider subset whose shared config patches should be applied.
None means "all providers with patches". An empty set disables
vault config patching entirely. terok uses this to gate experimental
OAuth routing without affecting standalone executor defaults.
disabled_vault_patch_providers = None
class-attribute
instance-attribute
¶
Provider subset whose previously managed config patch values should
be removed if still owned by terok. None removes nothing.
expose_credential_providers = frozenset()
class-attribute
instance-attribute
¶
Providers whose credential file should remain writable in-container.
By default every provider with a vault.credential_file
gets the file mounted read-only on top of its shared config dir, so an
in-container /login cannot taint the host copy
(terok-ai/terok#873).
Providers in this set keep the writable bind — used by terok's
experimental expose_oauth_token mode where the agent intentionally
manages its own token.
unrestricted = True
class-attribute
instance-attribute
¶
Enable auto-approve flags for all agents.
timezone = None
class-attribute
instance-attribute
¶
IANA timezone name propagated to the container as TZ.
None (the default) means detect the host's timezone via
terok_executor._util.detect_host_timezone — the container
then follows the host. Pass an explicit string ("UTC",
"Europe/Prague") to override, including to pin the container to
UTC for reproducible runs. If neither detection nor an override
yields a zone, TZ is not set and the image default applies.
agent_config_dir = None
class-attribute
instance-attribute
¶
Pre-prepared agent config directory (→ /home/dev/.terok:Z).
shared_dir = None
class-attribute
instance-attribute
¶
Host-side shared directory. Created by the assembly function if set.
shared_mount = '/shared'
class-attribute
instance-attribute
¶
Container-side mount point for the shared directory.
task_dir = None
class-attribute
instance-attribute
¶
Host-side task directory. A temp dir is created if None.
envs_dir = None
class-attribute
instance-attribute
¶
Base directory for shared config mounts. Uses paths.mounts_dir
if None.
extra_volumes = ()
class-attribute
instance-attribute
¶
Additional volume specs from the caller (e.g. SSH mounts from terok).
AgentRunner(*, sandbox=None, runtime=None, roster=None, base_image='fedora:44', family=None, cfg=None)
¶
Composes sandbox + agent config into a single container launch.
All three run methods follow the same flow:
- Ensure L0+L1 images exist (build if missing)
- Prepare agent-config directory (wrapper, instructions, prompt)
- Assemble environment variables and volume mounts
- Optionally set up gate (mirror repo, create token)
- Launch container via podman
Source code in src/terok_executor/container/runner.py
sandbox
property
¶
Lazy-init sandbox facade.
When an explicit runtime was supplied but no sandbox, the
sandbox is constructed with that same runtime so the two share
one backend instance.
runtime
property
¶
Return the container runtime used for observation and lifecycle.
Falls back to the sandbox's runtime when the caller did not supply one — keeps the two in sync by construction.
roster
property
¶
Lazy-init agent roster.
run_headless(provider, repo, *, prompt, branch=None, model=None, max_turns=None, timeout=1800, gate=True, name=None, follow=False, unrestricted=True, gpu=False, memory=None, cpus=None, hooks=None, human_name=None, human_email=None, authorship=None, shared_dir=None, shared_mount='/shared', timezone=None, project_id='', task_id='', dossier_path=None)
¶
Launch a headless agent run. Returns container name.
The agent executes the prompt against repo (local path or git URL) and exits when done or when timeout is reached. Set follow=True to block until the agent finishes (the CLI does this by default).
project_id, task_id, dossier_path propagate the terok orchestrator's identity into the per-container supervisor sidecar. Defaults preserve the standalone-executor case (no terok above).
Source code in src/terok_executor/container/runner.py
run_interactive(provider, repo, *, branch=None, gate=True, name=None, unrestricted=True, gpu=False, memory=None, cpus=None, hooks=None, human_name=None, human_email=None, authorship=None, shared_dir=None, shared_mount='/shared', timezone=None, project_id='', task_id='', dossier_path=None)
¶
Launch an interactive container. Returns container name.
The container stays up after init; user logs in via podman exec.
See run_headless
for the project_id / task_id / dossier_path semantics.
Source code in src/terok_executor/container/runner.py
run_web(repo, *, port=None, branch=None, gate=True, name=None, public_url=None, unrestricted=True, gpu=False, memory=None, cpus=None, hooks=None, human_name=None, human_email=None, authorship=None, shared_dir=None, shared_mount='/shared', timezone=None, project_id='', task_id='', dossier_path=None)
¶
Launch a toad web container. Returns container name.
If port is None, an available port is auto-allocated.
See run_headless
for the project_id / task_id / dossier_path semantics.
Source code in src/terok_executor/container/runner.py
run_tool(tool, repo, *, tool_args=(), branch=None, gate=True, name=None, follow=True, timeout=600, timezone=None, project_id='', task_id='', dossier_path=None)
¶
Launch a sidecar tool container. Returns container name.
Runs the named tool in a lightweight sidecar L1 image (no agent CLIs). The tool receives the real API key from the credential store — not a phantom token.
See run_headless
for the project_id / task_id / dossier_path semantics.
Source code in src/terok_executor/container/runner.py
launch_prepared(*, env, volumes, image, command, name, task_dir, gpu=False, memory=None, cpus=None, unrestricted=True, sealed=False, hooks=None, extra_args=None, hostname=None, annotations=None, runtime=None, project_id='', task_id='', dossier_path=None, per_container=None)
¶
Launch a container from a caller-prepared env, volumes, image, and command.
Use this when the caller has already assembled the environment and
volume specs — e.g. the terok orchestrator, which computes
project-specific env via build_task_env_and_volumes and owns
the container naming policy. For end-to-end runs from a repo and
prompt (CLI-style), use run_headless, run_interactive,
or run_web instead.
In sealed isolation mode (sealed=True), the sandbox splits the
launch into create → copy_to → start instead of a
single run — no host↔container bind mounts remain after startup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
env
|
dict[str, str]
|
Environment variables injected into the container. |
required |
volumes
|
list[VolumeSpec]
|
Host↔container directory specs (sandbox decides mount vs inject). |
required |
image
|
str
|
Image tag to run. |
required |
command
|
list[str]
|
Command + args to execute as PID 1. |
required |
name
|
str
|
Container name (must be unique on the host). |
required |
task_dir
|
Path
|
Per-task directory used for per-container shield state. |
required |
gpu
|
bool
|
Pass GPU device args when True. |
False
|
memory
|
str | None
|
Podman |
None
|
cpus
|
str | None
|
Podman |
None
|
unrestricted
|
bool
|
When False, adds |
True
|
sealed
|
bool
|
Enable sealed isolation (no bind mounts). |
False
|
hooks
|
LifecycleHooks | None
|
Optional lifecycle callbacks fired around the launch. |
None
|
extra_args
|
list[str] | None
|
Additional raw |
None
|
hostname
|
str | None
|
Override the in-container hostname (podman |
None
|
annotations
|
Mapping[str, str] | None
|
OCI annotations forwarded as |
None
|
runtime
|
str | None
|
OCI runtime selector forwarded to
|
None
|
project_id
|
str
|
Identity written into the per-container
supervisor sidecar so the supervisor can scope its
state to the calling terok project. Default |
''
|
task_id
|
str
|
Per-task identity written into the supervisor
sidecar alongside project_id. Default |
''
|
dossier_path
|
Path | str | None
|
Path to the per-task dossier file the
shield reads at container start. Default |
None
|
per_container
|
PerContainerResources | None
|
Pre-allocated per-container socket dir / TCP
ports. When provided, the launch uses these instead of
allocating its own — so a caller that already threaded
the same instance through env assembly
( |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The container name (same as name). |
Raises:
| Type | Description |
|---|---|
BuildError
|
When GPU was requested but the host has no functioning NVIDIA CDI. |
Source code in src/terok_executor/container/runner.py
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 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 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 | |
wait_for_exit(container_name, timeout=None)
¶
Block until container_name exits; return its exit code.
Raises TimeoutError when timeout elapses before the
container exits — signalled out of band so a container that
legitimately exits with code 124 (the timeout(1) convention)
is returned unambiguously as its real exit code, not conflated
with the wait timing out.
Raises RuntimeError when podman wait itself fails
(non-zero returncode, e.g. unknown container) or returns output
that is not a container exit code — the podman error is never
impersonated as the container's exit code, which would let a
"no such container" diagnostic leak out as exit code 125.
Raises FileNotFoundError when podman is not on PATH.
Intentionally re-implements the wait loop instead of delegating
to Sandbox.wait_for_exit, which swallows
subprocess.TimeoutExpired and returns the 124 sentinel
— fine for fire-and-forget generic waits, lossy for task-level
callers that need to record the real exit code.
Source code in src/terok_executor/container/runner.py
logs(container_name, *, tail=None, timestamps=False, since=None)
¶
Return the container's logged output as a single string.
One-shot retrieval for the "just show me what ran" case. For live
streaming (human watching), use stream_logs_process; for
archival, use capture_logs.
Raises RuntimeError when podman logs returns a non-zero
status (e.g. unknown container) — the diagnostic is surfaced rather
than impersonated as empty output. FileNotFoundError
propagates when podman is not on PATH.
Source code in src/terok_executor/container/runner.py
capture_logs(container_name, dest, *, timestamps=True, timeout=60.0)
¶
Capture a container's logs to dest; return True on success.
Streams stdout directly to dest (bytes) so large logs do not need to fit in memory. Used at task-archive time to freeze the container's output onto the host filesystem before removal.
On any failure — missing podman, podman error, timeout — dest is
removed and False is returned so the caller sees one signal,
not a partially-written file.
Source code in src/terok_executor/container/runner.py
stream_logs_process(container_name, *, follow=False, tail=None, timestamps=False, merge_stderr=False)
¶
Spawn a long-running podman logs process; return the Popen.
The raw subprocess handle is exposed deliberately: live-log
consumers (TUI log viewer, interactive task logs -f) need
fd-level control — select() between reads, SIGINT handling,
stop-event polling — that a higher-level iterator abstraction
would hide badly. Every current caller's event loop already looks
like select([proc.stdout], …) → read1() so returning the
Popen matches existing patterns instead of fighting them.
Caller owns the subprocess. Typical pattern::
proc = runner.stream_logs_process(cname, follow=True)
try:
for chunk in iter(proc.stdout.read1, b""):
...
finally:
proc.terminate()
proc.wait()
When merge_stderr is True, stderr is folded into stdout
(matches subprocess.STDOUT); otherwise stderr is a separate
pipe the caller can drain.
FileNotFoundError propagates when podman is not on
PATH — callers handle it (usually as a user-facing "podman not
installed" error).
Source code in src/terok_executor/container/runner.py
Authenticator(provider)
dataclass
¶
Vendor-credential acquisition for a single agent.
Wraps the authenticate flow behind a stable class so callers
that orchestrate a multi-step setup (terok project init, the
standalone terok-executor auth command, the TUI auth flow)
talk to one named surface bound to self.provider.
The discovery counterparts
(list_authenticated_agents,
scan_leaked_credentials)
stay as module-level fns in their owning submodules — folding them
in here would create a tach cycle through terok_executor.acp
and terok_executor.credentials.vault_commands, which already
depend on this module transitively.
provider
instance-attribute
¶
Auth provider name (e.g. "claude").
run(project_id, *, mounts_dir, image=None, expose_token=False, oauth_enabled=True, credential_set='default')
¶
Run the auth flow for self.provider; see module-level docs.
Mirrors the parameters of the underlying authenticate free
function — instance-bound self.provider replaces the old
positional provider arg.
Source code in src/terok_executor/credentials/auth.py
prepare_oauth(project_id, *, mounts_dir, image, expose_token=False, credential_set='default')
¶
Build an AuthSession without running it.
Frontends that own their own UI loop (e.g. the terok Textual TUI,
which wants to dispatch the OAuth container into a new terminal
tab or via tmux instead of inline) build the session here, run
session.argv however they like, then call session.capture()
on success. The CLI's blocking authenticate path is just
another such caller — see _run_auth_container.
Source code in src/terok_executor/credentials/auth.py
AuthSession(provider, project_id, container_name, argv, banner, auth_dir, mounts_dir, credential_set='default', expose_token=False, _tmpdir=None)
dataclass
¶
A prepared-but-not-run OAuth auth container session.
Built by Authenticator.prepare_oauth
(or the module-level prepare_oauth_session
helper). Hold-don't-call: the caller is responsible for running
argv (synchronously, in a new terminal tab, suspended TUI, etc.)
and calling capture() afterwards. Use as a context manager so
the temp dir and any dangling container are cleaned up on exit.
provider
instance-attribute
¶
Provider descriptor (label, banner hint, mount points).
project_id
instance-attribute
¶
Project scope for the banner; None for host-wide auth.
container_name
instance-attribute
¶
Podman container name (used for cleanup and -it log clarity).
argv
instance-attribute
¶
The podman run … command line — run this however you like.
banner
instance-attribute
¶
Banner text to display before launching argv.
auth_dir
instance-attribute
¶
Temp dir bind-mounted as the container's auth config target.
Lives until cleanup() (or __exit__). Credential extraction
in capture() reads from here, so don't remove it manually.
mounts_dir
instance-attribute
¶
Base directory for the shared post-capture mount (OAuth providers only).
credential_set = 'default'
class-attribute
instance-attribute
¶
Which credential set in the vault DB receives the captured token.
expose_token = False
class-attribute
instance-attribute
¶
When True, real credential files are copied into the shared mount (tier 3).
title
property
¶
Short human-readable title ("Authenticating Claude (host-wide)").
capture()
¶
Extract credentials from auth_dir, store them in the vault DB.
Call after argv exits successfully. Safe to call multiple
times (the underlying extractor is idempotent on a stable
credential file).
Source code in src/terok_executor/credentials/auth.py
cleanup()
¶
Release the temp dir and force-remove any lingering container.
Idempotent. __exit__ calls this automatically.
Source code in src/terok_executor/credentials/auth.py
__enter__()
¶
KrunHost(*, cfg=None)
¶
Host-side krun launch context — vault keypair + runtime + launch args.
One instance per launch. The first access to
keypair opens the vault DB
and materialises the %host private/public keypair to tmpfs;
every subsequent access on the same instance reuses the cached
result, so calling both runtime
and launch_args pays
that cost only once.
Requires the vault to be unlocked — the krun runtime is gated on
experimental: true upstream and assumes the operator has the
vault open for the session. A NoPassphraseError from the
underlying vault open propagates unchanged so the orchestrator can
render its own remediation hint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cfg
|
SandboxConfig | None
|
Sandbox config used to open the credential DB. |
None
|
Bind a krun launch to cfg; the keypair is loaded on first access.
Source code in src/terok_executor/krun.py
__slots__ = ('_cfg', '_keypair')
class-attribute
instance-attribute
¶
keypair
property
¶
Vault-backed %host keypair materialised to tmpfs (cached).
First access opens the vault DB and writes the OpenSSH-PEM private + public-key line to a tmpfs cache directory; subsequent accesses on the same instance return the same object without reopening the vault.
runtime()
¶
Construct a production KrunRuntime in one call.
Wires together the three production pieces — the cached host
keypair, the TCP-over-passt SSH transport, and a fresh
PodmanRuntime for lifecycle.
The experimental-flag gate stays on the orchestrator side (this
factory is reachable only when the gate is open).
Source code in src/terok_executor/krun.py
launch_args()
¶
Extra podman run args terok must splice in for a krun launch.
Four things that all reach across the orchestrator/runtime
boundary into executor's domain — the L0 image, the host
keypair, the in-guest init-ssh-and-repo.sh, and the DNS
forwarder address — so they live here together rather than
being open-coded in terok's _project_runtime_flags:
- Bind-mount the live host pubkey over the L0's empty
placeholder at
/etc/ssh/authorized_keys.d/terok.zis the shared SELinux relabel (neverZ— the host pubkey is host-wide and concurrent containers share the source). - Set
TEROK_CONTAINER_RUNTIME=krunso the init script's krun gate fires. - Override the L0's
USER devdirective with--user rootso the in-guest sshd can start, listen on TCP 22, and drop to the authenticated user on connection.USER devis the right default under crun (AI agents that refuse uid 0); under krun the session uid comes from whichssh user@…the operator picks. --dns 169.254.1.1— kept for shield-bypass; under shield-up the bind-mounted resolv.conf overrides this anyway.
Doesn't include --runtime krun itself or krun's microVM-sizing
annotations — those are orchestrator-level decisions terok keeps.
Source code in src/terok_executor/krun.py
KrunHostKeypair(private_path, public_path, public_line, fingerprint, created)
dataclass
¶
Materialised view of the %host infrastructure keypair.
Returned by ensure_krun_host_keypair.
Carries the tmpfs path to the OpenSSH-PEM private key (ready for
ssh -i) and the matching public-key file (ready to bind-mount
into the krun guest at /etc/ssh/authorized_keys.d/terok), so
callers don't have to redo the DER→PEM conversion or re-derive
the public line from raw blobs.
private_path
instance-attribute
¶
tmpfs path holding the OpenSSH-PEM private key (0600 perms).
public_path
instance-attribute
¶
Sibling .pub file (0644 perms) carrying the public line.
public_line
instance-attribute
¶
Single-line OpenSSH public key (ssh-ed25519 AAAA… comment).
fingerprint
instance-attribute
¶
Canonical SHA256:… fingerprint over the SSH wire-format blob.
created
instance-attribute
¶
True when this call minted the key; False when it was loaded.
AgentConfigSpec(tasks_root, task_id, subagents, selected_agents=None, prompt=None, provider='claude', instructions=None, default_agent=None, mounts_base=None)
dataclass
¶
Groups parameters for preparing an agent-config directory.
tasks_root
instance-attribute
¶
task_id
instance-attribute
¶
subagents
instance-attribute
¶
selected_agents = None
class-attribute
instance-attribute
¶
prompt = None
class-attribute
instance-attribute
¶
provider = 'claude'
class-attribute
instance-attribute
¶
instructions = None
class-attribute
instance-attribute
¶
default_agent = None
class-attribute
instance-attribute
¶
mounts_base = None
class-attribute
instance-attribute
¶
__post_init__()
¶
Coerce mutable sequences to tuples for true immutability.
Defensive against callers that build the spec from
json.loads / yaml.load output where the runtime types are
list instead of tuple. Mypy sees the static annotations
and reports the isinstance(..., list) branches as unreachable;
the runtime coercion remains correct.
Source code in src/terok_executor/provider/agents.py
AgentProvider(name, label, binary, git_author_name, git_author_email, headless_subcommand, prompt_flag, auto_approve_env, auto_approve_flags, output_format_flags, model_flag, max_turns_flag, verbose_flag, supports_session_resume, resume_flag, continue_flag, session_file, supports_agents_json, supports_session_hook, supports_add_dir, log_format, opencode_config=None, refuse_subcommands=())
dataclass
¶
Describes how to run one AI coding agent (all modes: interactive + headless).
name
instance-attribute
¶
Short key used in CLI dispatch (e.g. "claude", "codex").
label
instance-attribute
¶
Human-readable display name (e.g. "Claude", "Codex").
binary
instance-attribute
¶
CLI binary name (e.g. "claude", "codex", "opencode").
git_author_name
instance-attribute
¶
AI identity name for Git author/committer policy application.
git_author_email
instance-attribute
¶
AI identity email for Git author/committer policy application.
headless_subcommand
instance-attribute
¶
Subcommand for headless mode (e.g. "exec" for codex, "run" for opencode).
None means the binary uses flags only (e.g. claude -p).
prompt_flag
instance-attribute
¶
Flag for passing the prompt.
"-p" for flag-based, "" for positional (after subcommand).
auto_approve_env
instance-attribute
¶
Environment variables for fully autonomous execution.
Injected into the container env by _apply_unrestricted_env() when
TEROK_UNRESTRICTED=1. Read by agents regardless of launch path.
Claude uses /etc/claude-code/managed-settings.json instead.
auto_approve_flags
instance-attribute
¶
CLI flags injected by the shell wrapper when TEROK_UNRESTRICTED=1.
Only for agents that lack an env var or managed config mechanism
(currently Codex only). Empty for all other agents — their env vars
and /etc/ config files handle permissions across all launch paths.
output_format_flags
instance-attribute
¶
Flags for structured output (e.g. ("--output-format", "stream-json")).
model_flag
instance-attribute
¶
Flag for model override ("--model", "--agent", or None).
max_turns_flag
instance-attribute
¶
Flag for maximum turns ("--max-turns" or None).
verbose_flag
instance-attribute
¶
Flag for verbose output ("--verbose" or None).
supports_session_resume
instance-attribute
¶
Whether the provider supports resuming a previous session.
resume_flag
instance-attribute
¶
Flag to resume a session (e.g. "--resume", "--session").
continue_flag
instance-attribute
¶
Flag to continue a session (e.g. "--continue").
session_file
instance-attribute
¶
Filename in /home/dev/.terok/ for stored session ID.
Providers that capture session IDs via plugin or post-run parsing set this
to a filename (e.g. "opencode-session.txt"). Providers with their own
hook mechanism (Claude) or no session support set this to None.
supports_agents_json
instance-attribute
¶
Whether the provider supports --agents JSON (Claude only).
supports_session_hook
instance-attribute
¶
Whether the provider supports SessionStart hooks (Claude only).
supports_add_dir
instance-attribute
¶
Whether the provider supports --add-dir "/" (Claude only).
log_format
instance-attribute
¶
Log format identifier: "claude-stream-json" or "plain".
opencode_config = None
class-attribute
instance-attribute
¶
Configuration for OpenCode-based providers (Blablador, KISSKI, etc.).
When set, this provider uses OpenCode with a custom OpenAI-compatible API. The configuration includes API endpoints, model preferences, and provider-specific settings that are injected into the container environment.
refuse_subcommands = ()
class-attribute
instance-attribute
¶
Subcommands the in-container wrapper refuses with a friendly error.
Used to block credential-handling flows (login, logout,
setup-token) that would otherwise pollute the host-shared mount —
operators authenticate on the host via terok auth instead. Best
effort only; the firewall is the actual enforcement
(terok-ai/terok#873).
uses_opencode_instructions
property
¶
Whether the provider uses OpenCode's instruction system.
apply_config(config, overrides=None)
¶
Resolve config values for this provider with best-effort feature mapping.
CLI flag overrides take precedence over config values. When this provider lacks a feature, an analogue is used where possible (e.g. injecting max-turns guidance into the prompt), and a warning is emitted for features that have no analogue.
Source code in src/terok_executor/provider/providers.py
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | |
build_headless_command(*, timeout, model=None, max_turns=None)
¶
Assemble the bash command string for a headless agent run.
The command assumes:
init-ssh-and-repo.shhas already set up the workspace- The prompt is in
/home/dev/.terok/prompt.txt - For Claude, the
claude()wrapper function is sourced viabash -l
Returns a bash command string suitable for ["bash", "-lc", cmd].
Dispatches to provider-specific assembly: Claude routes through the
shell wrapper (which adds --add-dir, --agents, git env);
everything else uses the generic shape with subcommand + flags.
Source code in src/terok_executor/provider/providers.py
CLIOverrides(model=None, max_turns=None, timeout=None, instructions=None)
dataclass
¶
CLI flag overrides for a headless agent run.
model = None
class-attribute
instance-attribute
¶
Explicit --model from CLI (takes precedence over config).
max_turns = None
class-attribute
instance-attribute
¶
Explicit --max-turns from CLI.
timeout = None
class-attribute
instance-attribute
¶
Explicit --timeout from CLI.
instructions = None
class-attribute
instance-attribute
¶
Resolved instructions text. Delivery is provider-aware.
AgentRoster(_providers=dict(), _auth_providers=dict(), _vault_routes=dict(), _sidecar_specs=dict(), _installs=dict(), _helps=dict(), _mounts=(), _agent_names=(), _all_names=(), _web_ingress=frozenset())
dataclass
¶
Queryable view over the loaded set of agents and tools.
Returned by load_roster;
grouped accessors expose providers, auth providers, vault routes,
sidecar specs, install snippets, and help blurbs by name.
providers
property
¶
All headless agent providers (kind: agent only).
auth_providers
property
¶
All auth providers (agents + tools with auth: section).
vault_routes
property
¶
All vault routes, keyed by provider name.
sidecar_specs
property
¶
All sidecar tool specs, keyed by tool name.
agent_names
property
¶
Names of kind: agent entries (for CLI completion).
all_names
property
¶
Names of all entries (agents + tools).
installs
property
¶
All install specs, keyed by roster name (entries without one are absent).
helps
property
¶
All help blurbs, keyed by roster name (entries without one are absent).
web_ingress
property
¶
Names of entries that publish a host HTTP port (web_ingress: true).
Consumers (e.g. terok's task launcher) use this to decide whether to allocate a published port and drop a per-task auth token into the container-visible config dir.
mounts
property
¶
All shared directory mounts (auth dirs + explicit mounts: sections).
Deduplicated by host_dir — if auth and mounts define the same
directory, only one entry is returned.
resolve_selection(selection)
¶
Resolve a user-supplied selection into the full set of roster names to install.
Accepts the literal string "all" (every roster entry that has an
InstallSpec) or a tuple of
selection tokens. Each token is either a roster name (include) or a
name prefixed with - (exclude). The pseudo-name "all" is also
valid as an include token, meaning "seed from every installable
entry"; this combines naturally with excludes, e.g. ("all",
"-vibe") installs everything except vibe. When no include tokens
are present (only excludes), the seed is the full roster.
Includes are expanded transitively via depends_on before
excludes are applied, so an exclude that names a dependency of a
kept agent will silently drop that dependency — likely producing a
broken image, but matching the user's literal request.
Returns the names sorted alphabetically — the canonical order used for the OCI label, the tag suffix, and the in-container manifest.
Raises ValueError if a requested include or exclude name is not
in the roster, or TypeError if selection is a string other
than "all" (a bare name like "claude" would otherwise be
iterated into characters). Excludes that name a known agent but
don't appear in the resolved include set are a no-op.
Source code in src/terok_executor/roster/loader.py
get_provider(name, *, default_agent=None)
¶
Resolve a provider name to an AgentProvider.
Falls back to default_agent, then "claude".
Raises SystemExit if the resolved name is unknown.
Source code in src/terok_executor/roster/loader.py
get_auth_provider(name)
¶
Look up an auth provider by name.
Raises SystemExit if the name is unknown.
Source code in src/terok_executor/roster/loader.py
get_sidecar_spec(name)
¶
Look up a sidecar spec by tool name.
Raises SystemExit if the name has no sidecar configuration.
Source code in src/terok_executor/roster/loader.py
generate_routes_json()
¶
Generate the routes.json content for the sandbox vault server.
Returns a JSON object mapping provider name → VaultRouteEntry
with empty/absent optional fields stripped.
Source code in src/terok_executor/roster/loader.py
collect_all_auto_approve_env()
¶
Merge auto_approve.env from all providers into one dict.
Source code in src/terok_executor/roster/loader.py
collect_opencode_provider_env()
¶
Collect env vars for all OpenCode-based providers.
Source code in src/terok_executor/roster/loader.py
shared()
staticmethod
¶
Return the process-wide cached roster.
Loaded on first access; every subsequent call returns the same
instance. Use this from anywhere that just needs the global
view; tests that mutate or replace the roster should call
load_roster and
keep the result local.
Source code in src/terok_executor/roster/loader.py
parse_selection(raw)
staticmethod
¶
Normalise a user-supplied agent selection string.
Accepts a comma-list of selection tokens or the literal "all".
Each token is either an agent name ("claude") or a name
prefixed with - to exclude it from the selection
("-vibe"). The pseudo-name "all" is also valid as a
token, so "all,-vibe" means "everything except vibe". When
the input contains only excludes ("-vibe"), the selection
seeds from every installable entry — same effect as
"all,-vibe".
Whitespace is stripped, empty / whitespace-only entries dropped,
and case folded. Empty or all-whitespace input collapses to
"all" — the same shape
AgentRoster.resolve_selection
expects. Unknown names are not checked here;
resolve_selection does that.
Source code in src/terok_executor/roster/loader.py
validate_selection(raw)
¶
Reject raw with SystemExit(2) if it names roster entries we don't have.
CLI-flavoured: prints a Invalid agent selection: … line on
stderr and exits. Domain callers that just want the parsed
tuple should use
parse_selection
+ resolve_selection
and handle ValueError themselves.
Source code in src/terok_executor/roster/loader.py
prompt_selection()
¶
Print the installed roster and read one line of executor grammar.
Empty input → "all". Non-interactive stdin (closed pipe)
exits with a hint to pass the selection positionally instead.
Source code in src/terok_executor/roster/loader.py
ensure_vault_routes(cfg=None)
¶
Generate routes.json from this roster and write it to disk.
The routes file is written to the path configured in
SandboxConfig (typically
~/.local/share/terok/vault/routes.json).
When cfg is None, falls back to standalone defaults.
Returns the path to the written file.
Source code in src/terok_executor/roster/loader.py
doctor_checks(*, token_broker_port=None)
¶
Return agent-level health checks for in-container diagnostics.
Delegates to
terok_executor.doctor for the actual
check factories; this method is the canonical entry point so
consumers can discover the checks through the roster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token_broker_port
|
int | None
|
Host-side vault broker TCP port. |
None
|
Source code in src/terok_executor/roster/loader.py
SharedMountStorageInfo(name, label, bytes)
dataclass
¶
Disk usage for one shared config mount directory.
name
instance-attribute
¶
label
instance-attribute
¶
bytes
instance-attribute
¶
measure_all(mounts_base=None)
classmethod
¶
Measure each shared config mount directory.
Labels come from the agent roster when available, falling back to a title-cased version of the directory name.
Source code in src/terok_executor/storage.py
TaskStorageInfo(task_id, workspace_bytes, agent_config_bytes)
dataclass
¶
Disk usage snapshot for a single task's host directories.
task_id
instance-attribute
¶
workspace_bytes
instance-attribute
¶
agent_config_bytes
instance-attribute
¶
total_bytes
property
¶
Combined footprint of workspace and agent config.
measure(task_dir)
classmethod
¶
Measure a single task's disk footprint.
Expects the standard layout: <task_dir>/workspace-dangerous/
for the agent-writable code, <task_dir>/agent-config/ for
per-task configuration.
Source code in src/terok_executor/storage.py
measure_all(tasks_root)
classmethod
¶
Measure every task under tasks_root, sorted by task ID.
Source code in src/terok_executor/storage.py
acp_socket_is_live(path)
¶
Return True when a peer is currently accepting on path.
Distinguishes a live ACP daemon from a stale socket file left
behind by a crash: a successful connect means a peer is
listening, while ECONNREFUSED (and any other OSError)
means the file is safe to unlink.
Source code in src/terok_executor/acp/daemon.py
list_authenticated_agents(*, db_path=None, scope=DEFAULT_CREDENTIAL_SCOPE)
¶
Return provider names that have stored credentials in scope.
Pure query against CredentialDB — no probing,
no container exec. Used by the host-side acp list to classify
endpoints in its status display; the roster itself doesn't gate
probing on this anymore (file-based auth like Claude's OAuth lives
outside the vault, so a vault-only filter would silently hide
working agents).
Source code in src/terok_executor/acp/roster.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
seed_workspace_from_clone_cache(workspace_path, scope, *, origin_url=None, cfg=None)
¶
Pre-populate workspace_path from the clone cache for scope.
Returns True if the workspace was successfully seeded.
After copying, rewrites the git origin remote to origin_url so that
the in-container init script's sanity check (which compares origin
against CODE_REPO) passes — the cache's origin points to a local
file:// URL that won't match.
Skips seeding when the cache doesn't exist, the workspace already
contains a .git directory, or the copy fails. Failures are
logged and swallowed — the container falls back to a full clone.
Source code in src/terok_executor/container/cache.py
assemble_container_env(spec, roster, *, caller_manages_vault=False, per_container=None)
¶
Assemble container environment variables and volume mounts.
This is the single source of truth for container env/volume assembly.
Both AgentRunner._run() and terok's build_task_env_and_volumes()
delegate here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
ContainerEnvSpec
|
What the caller wants — all host↔container contract fields. |
required |
roster
|
AgentRoster
|
Agent roster for shared mounts, vault routes, provider identity. |
required |
caller_manages_vault
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
ContainerEnvResult
|
Assembled env dict, volume tuple, and resolved task_dir. |
Source code in src/terok_executor/container/env.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |
inject_prompt(container_name, prompt_text)
¶
Write a follow-up prompt into a stopped sealed container.
Writes prompt_text to a temp file and copies it into the container
via podman cp. Works on stopped containers (unlike podman exec),
which is the expected state during headless follow-ups.
Source code in src/terok_executor/container/inject.py
prepare_oauth_session(provider, project_id, *, mounts_dir, image, expose_token=False, credential_set='default')
¶
Build an AuthSession without running it.
Creates a fresh temp dir, computes the podman run argv, and
cleans up any leftover container of the same name (so re-auth
after a previous abort isn't blocked). The caller drives execution
and credential capture; see AuthSession.
The temp dir uses a clean slate so the vendor auth flow re-runs end to end — no stale config, no cached sessions.
Source code in src/terok_executor/credentials/auth.py
store_api_key(provider, api_key, credential_set='default')
¶
Store an API key directly in the credential DB (no container needed).
This is the non-interactive fast path for automated workflows and CI.
The key is stored as {"type": "api_key", "key": "<value>"}.
Source code in src/terok_executor/credentials/auth.py
scan_leaked_credentials(mounts_base)
¶
Return (provider, host_path) for credential files found in shared mounts.
When the vault is active, real secrets should only live in the vault's sqlite DB — not in the shared config directories that get mounted into containers. This function checks each routed provider's mount for credential files that would leak real tokens alongside phantom ones.
Files injected by _write_claude_credentials_file
are recognised by their dummy accessToken marker and skipped.
Symlinks are rejected to prevent a container from tricking the scan into reading arbitrary host files via a crafted symlink in the shared mount.
Source code in src/terok_executor/credentials/vault_commands.py
ensure_krun_host_keypair(*, cfg=None, runtime_dir=None)
¶
Load (or mint, first call) the %host keypair and materialise it to tmpfs.
The vault is the system of record: the keypair lives in the sandbox
credential DB under the %host infrastructure scope. This
helper opens the DB, calls
ensure_infra_keypair (which
generates the key on first call and reloads it thereafter), and
writes the OpenSSH-PEM private + the public-key line into
runtime_dir (default:
namespace_runtime_dir()).
The orchestrator bind-mounts public_path into the running
krun guest at /etc/ssh/authorized_keys.d/terok so the
guest's sshd accepts our private key. The L0 image itself ships
an empty placeholder at that path; the bind-mount overlays it.
Rotation = clear the %host scope in the vault, then re-run.
Typically called per task launch under krun (idempotent — loads
on subsequent calls). New tasks pick up the new key; in-flight
tasks keep what they had until they're stopped.
Requires the vault to be unlocked — the krun runtime is gated on
experimental: true upstream and assumes the operator has the
vault open for the session. A NoPassphraseError propagates
unchanged so the orchestrator can render its own remediation hint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cfg
|
SandboxConfig | None
|
Sandbox config used to open the credential DB. |
None
|
runtime_dir
|
Path | None
|
Override for the tmpfs cache directory. |
None
|
Source code in src/terok_executor/krun.py
parse_md_agent(file_path)
¶
Parse a .md file with YAML frontmatter into an agent dict.
Expected format
name: agent-name description: ... tools: [Read, Grep] model: sonnet
System prompt body...
Source code in src/terok_executor/provider/agents.py
prepare_agent_config_dir(spec)
¶
Create and populate the agent-config directory for a task.
Writes:
- terok-executor.sh (always) — wrapper functions with git env vars
- agents.json (only when provider supports it and sub-agents are non-empty)
- prompt.txt (if prompt given, headless only)
- instructions.md (always) — custom instructions or a neutral default
- instructions path injected into shared
OpenCode and Blablador configs
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
AgentConfigSpec
|
All agent-config parameters bundled in an |
required |
Returns the agent_config_dir path.
Source code in src/terok_executor/provider/agents.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
bundled_default_instructions()
¶
Read and return the bundled default instructions from package resources.
Source code in src/terok_executor/provider/instructions.py
resolve_instructions(config, provider_name, project_root=None)
¶
Resolve instructions from a merged config dict.
Supports:
- Flat string: returned as-is
- Per-provider dict: uses resolve_provider_value, falls back to _default
- List (with _inherit): splices bundled default at each _inherit sentinel
- Absent/None: returns bundled default
After resolving the YAML value, appends the contents of
project_root/instructions.md (if it exists and is non-empty).
Returns the final instructions text.
Source code in src/terok_executor/provider/instructions.py
get_provider(name, *, default_agent=None)
¶
Resolve a provider name against the global AGENT_PROVIDERS registry.
Convenience wrapper around resolve_provider.
Source code in src/terok_executor/provider/providers.py
resolve_provider_value(key, config, provider_name)
¶
Extract a provider-aware config value.
Supports two forms:
- Flat value —
model: opus→ same for all providers. - Per-provider dict —
model: {claude: opus, codex: o3, _default: fast}→ looks up provider_name, falls back to_default, thenNone.
Returns None when the key is absent or has no match for the provider.
Null override behaviour: when a per-provider dict maps a provider to
null (Python None), that None is treated as "no value" and the
resolver falls back to _default. This is intentional — it allows a
lower-priority config layer to set a provider-specific value that a
higher-priority layer can effectively unset by mapping it to null,
letting the _default (or None) bubble up instead.
Internal to provider config resolution — full config-stack composition
(build_agent_config_stack, resolve_agent_config) lives in terok,
which owns the global/project/preset layer semantics.
Source code in src/terok_executor/provider/providers.py
ensure_sandbox_ready(*, cfg=None, no_vault=False, **aggregator_kwargs)
¶
Generate vault routes, then run the sandbox install aggregator.
Regenerating routes.json up front is what makes routing config
current before any launch — the per-container supervisor reads it
at container start. A bare aggregator call would leave a stale
routes.json in place and credential fetch would break on the
next terok-executor run until the operator remembers to run
vault routes.
no_vault gates the routes pre-step (if vault isn't being
touched, don't regenerate); other no_* flags flow through to
the aggregator.
Routes regeneration renders a Vault routes stage line so it
sits in the same column as the aggregator's own output rather
than failing silently above it — a corrupt YAML roster is the
most plausible reason for setup to fail before the aggregator
even starts, and a stage-shaped failure beats an unframed
traceback.