project
project
¶
Project entities, lifecycle, panic, SSH provisioning — public API surface.
Re-export catalog for everything project-shaped. Sources:
terok.lib.core.projects for the pure
ProjectConfig value type and discovery helpers;
terok.lib.domain.project for the rich
Project aggregate and lifecycle;
terok.lib.domain.project_state for
infrastructure-state queries;
terok.lib.domain.image_cleanup for
image listing & cleanup;
terok.lib.domain.panic for cross-project
lockdown;
terok.lib.domain.ssh for the SSH provisioning
workflow; and
terok.lib.domain.wizards.new_project
for the new-project wizard primitives that CLI prompts and TUI screens
share.
AGENTS_QUESTION = Question(key='agents', kind='multichoice', prompt='Select agents to install', help="Which AI coding agents to bake into this project's image, overriding the global default. Pick 'All agents' to inherit future additions, or enumerate specific agents to freeze the set.", required=True, choices_loader=_load_agent_choices, validate=_validate_agents)
module-attribute
¶
QUESTIONS = (Question(key='security_class', kind='choice', prompt='Select security mode', choices=(tuple(SECURITY_CLASSES)), required=True), Question(key='base', kind='choice', prompt='Select base image', choices=(tuple(BASES)), required=True), Question(key='project_id', kind='text', prompt='Project ID', required=True, transform=_slugify_project_id, validate=_validate_project_id, placeholder='lowercase; letters, digits, hyphens, underscores'), Question(key='upstream_url', kind='text', prompt='Upstream git URL', help='Leave empty for a local-only project (no remote).', placeholder='git@github.com:org/repo.git or https://…', default_visible=True), Question(key='default_branch', kind='text', prompt='Default branch', help="Leave empty to use the remote's default (or ``main`` when no remote).", placeholder='main', default_visible=True), Question(key='user_snippet', kind='editor', prompt='Custom image snippet', help='Optional Dockerfile fragment appended to the project image. Use for extra packages, env vars, or setup commands.', default_visible=True))
module-attribute
¶
__all__ = ['AGENTS_QUESTION', 'BrokenProject', 'DeleteProjectResult', 'Project', 'ProjectConfig', 'QUESTIONS', 'Question', 'cleanup_images', 'delete_project', 'derive_project', 'discover_projects', 'execute_panic', 'find_orphaned_images', 'find_projects_sharing_gate', 'format_panic_report', 'get_project', 'list_images', 'list_projects', 'load_project', 'panic_stop_containers', 'project_image_exists', 'remove_images', 'render_project_yaml', 'require_project_exists', 'set_project_image_agents', 'summarize_ssh_init', 'validate_answer', 'write_project_yaml']
module-attribute
¶
BrokenProject(id, config_path, error)
dataclass
¶
A project directory whose project.yml failed to load.
Carries just enough context for the TUI to render a row and show the
validation error in the details pane, without forcing callers to
re-run the failing load_project to rediscover the message.
ProjectConfig
¶
Bases: BaseModel
Resolved project configuration loaded from project.yml.
Pure value object — holds configuration fields with no behavior beyond
computed paths. The rich domain object Project
wraps this and provides behavior.
model_config = ConfigDict(frozen=True)
class-attribute
instance-attribute
¶
id
instance-attribute
¶
security_class
instance-attribute
¶
isolation = 'shared'
class-attribute
instance-attribute
¶
upstream_url
instance-attribute
¶
default_branch
instance-attribute
¶
root
instance-attribute
¶
tasks_root
instance-attribute
¶
gate_path
instance-attribute
¶
gate_enabled = True
class-attribute
instance-attribute
¶
staging_root
instance-attribute
¶
ssh_use_personal = False
class-attribute
instance-attribute
¶
Opt in to the user's ~/.ssh keys for host-side gate-sync (default off).
expose_external_remote = False
class-attribute
instance-attribute
¶
human_name = None
class-attribute
instance-attribute
¶
human_email = None
class-attribute
instance-attribute
¶
git_authorship = 'agent-human'
class-attribute
instance-attribute
¶
upstream_polling_enabled = True
class-attribute
instance-attribute
¶
upstream_polling_interval_minutes = 5
class-attribute
instance-attribute
¶
auto_sync_enabled = False
class-attribute
instance-attribute
¶
auto_sync_branches = Field(default_factory=list)
class-attribute
instance-attribute
¶
default_agent = None
class-attribute
instance-attribute
¶
default_login = None
class-attribute
instance-attribute
¶
agent_config = Field(default_factory=dict)
class-attribute
instance-attribute
¶
shutdown_timeout = 10
class-attribute
instance-attribute
¶
memory = None
class-attribute
instance-attribute
¶
Podman --memory value from run.memory in project.yml.
cpus = None
class-attribute
instance-attribute
¶
Podman --cpus value from run.cpus in project.yml.
nested_containers = False
class-attribute
instance-attribute
¶
Project runs podman/docker inside its container (see run.nested_containers).
runtime = None
class-attribute
instance-attribute
¶
OCI runtime selector from run.runtime.
None (default) means "use the global default", which itself
falls through to "crun" — the OCI runtime podman drives by
default on every supported distro. "krun" selects KVM-microVM
isolation; gated on the global experimental: true flag at
runtime selection time so a typo never silently boots the
experimental backend.
Sizing reuses the standard memory / cpus knobs — podman
writes them into the OCI spec and the runtime reads them there;
no krun-specific knob.
timezone = None
class-attribute
instance-attribute
¶
IANA timezone for task containers (from run.timezone).
None lets terok-executor fall back to the host's timezone; pass an
explicit string ("UTC", "Europe/Prague") to override — including
to pin containers to UTC for reproducible runs.
task_name_categories = None
class-attribute
instance-attribute
¶
shield_drop_on_task_run = True
class-attribute
instance-attribute
¶
shield_on_task_restart = 'retain'
class-attribute
instance-attribute
¶
hook_pre_start = None
class-attribute
instance-attribute
¶
hook_post_start = None
class-attribute
instance-attribute
¶
hook_post_ready = None
class-attribute
instance-attribute
¶
hook_post_stop = None
class-attribute
instance-attribute
¶
base_image = 'ubuntu:24.04'
class-attribute
instance-attribute
¶
family = None
class-attribute
instance-attribute
¶
Package family override for L0/L1 builds.
None lets terok-executor auto-detect from base_image; set
explicitly when the auto-detect allowlist doesn't recognise the
image (rocky, alma, suse, …).
agents = 'all'
class-attribute
instance-attribute
¶
Comma-separated roster entries to install in L1 (or "all").
snippet_inline = None
class-attribute
instance-attribute
¶
snippet_file = None
class-attribute
instance-attribute
¶
shared_dir = None
class-attribute
instance-attribute
¶
is_sealed
property
¶
Whether this project uses sealed isolation (zero bind mounts).
presets_dir
property
¶
Directory for preset config files for this project.
DeleteProjectResult
¶
Project(config)
¶
Rich project object — DDD Aggregate Root.
The primary domain object that callers interact with. Wraps a
ProjectConfig value object and exposes all project-scoped
operations through a natural OOP interface::
project = get_project("myproj")
task = project.create_task(name="fix-bug")
task.run_cli()
task.stop()
project.gate.sync()
Identity is based on project.id — two Project instances with
the same ID compare equal and hash identically, so they work correctly
in sets and dicts.
Subsystem access (gate, ssh, agents) uses lazy
initialization: the service objects are created on first property access
rather than at construction time. This avoids unnecessary I/O when only
a subset of functionality is needed. Uses __slots__ for memory
efficiency; cached_property is not available because it requires
__dict__.
Obtain via get_project or
list_projects.
Initialize with a resolved project configuration.
Source code in src/terok/lib/domain/project.py
__slots__ = ('_config', '_gate', '_ssh', '_agents')
class-attribute
instance-attribute
¶
id
property
¶
Return the project ID.
config
property
¶
Return the underlying configuration value object.
security_class
property
¶
Return the project's security class ('online' or 'gatekeeping').
tasks
property
¶
All tasks in this project — convenience for unfiltered iteration.
gate
property
¶
Return the project-scoped git gate manager (lazy-initialized).
ssh
property
¶
Return the project-scoped SSH manager (lazy-initialized).
needs_ssh_key_registration
property
¶
Return True when the upstream is SSH-scheme so a deploy key must be added.
Shared predicate used by the CLI pause helper and the TUI wizard's mid-flow "continue" gate — keeps the rule (SSH URLs need registration, HTTPS and no-upstream projects don't) in one place.
agents
property
¶
Return the project-scoped agent configuration manager (lazy-initialized).
__eq__(other)
¶
__hash__()
¶
create_task(*, name=None)
¶
Create a new task and return a rich Task entity.
Source code in src/terok/lib/domain/project.py
get_task(task_id)
¶
list_tasks(*, status=None, mode=None)
¶
Return all tasks, optionally filtered by status or mode.
Source code in src/terok/lib/domain/project.py
acp_endpoints()
¶
Return one ACPEndpoint per running task.
Cheap discovery surface — walks running tasks, classifies each
endpoint as active (daemon up, socket bound), ready
(task running with at least one authed agent, daemon would
spawn on first connect), or unsupported (no agents authed
for this task's image; connect would fail).
No probing, no socket traffic — one credential-DB read for
the whole listing, one Sandbox instance shared across
tasks, and image-label lookups memoised by image-id (most
tasks share an image). terok acp list and the TUI panel
share this entry point.
Source code in src/terok/lib/domain/project.py
run_headless(request)
¶
Create and run a headless task atomically. Returns the Task.
Source code in src/terok/lib/domain/project.py
followup_headless(task_id, prompt, follow=True)
¶
Send a follow-up prompt to a completed headless task.
Source code in src/terok/lib/domain/project.py
delete()
¶
generate_dockerfiles()
¶
build_images(*, include_dev=False, refresh_agents=False, full=False)
¶
Build container images for this project.
Source code in src/terok/lib/domain/project.py
state(*, gate_commit_provider=None)
¶
Return the project's infrastructure state snapshot.
gate_commit_provider is an optional callable that, given a
project id, returns the last gate commit dict (or None).
Used by the TUI to inject the live gate manager's last_commit
lookup without reaching for it from inside the helper.
Source code in src/terok/lib/domain/project.py
storage_detail()
¶
Return a detailed view of this project's on-disk footprint.
provision_ssh_key(*, key_type='ed25519', comment=None, force=False)
¶
Mint a vault-backed keypair and bind it to this project's scope.
Opens a fresh
SSHManager via the context-manager
form so the credential DB closes after init, then assigns the
new key_id to the project scope. Rendering the result is
the caller's job — see
summarize_ssh_init.
Source code in src/terok/lib/domain/project.py
register_ssh_key(key_id)
¶
Bind an already-minted key_id to this project (idempotent).
pause_for_ssh_key_registration_if_needed()
¶
Pause so the user can register the deploy key — only for SSH upstreams.
Source code in src/terok/lib/domain/project.py
list_presets()
¶
Question(key, kind, prompt, help='', choices=(), choices_loader=None, required=False, transform=None, validate=None, placeholder='', default_visible=False)
dataclass
¶
One wizard prompt — what to ask, how to validate, what shape the answer takes.
The presenter decides the visual treatment (numbered menu vs radio
buttons, input() vs Textual Input, $EDITOR vs TextArea);
the declaration here drives everything else.
key
instance-attribute
¶
Name of this field in the collected-values dict.
kind
instance-attribute
¶
Shape of the input — drives which widget / prompt style a presenter uses.
prompt
instance-attribute
¶
Short one-line question, used as both CLI prompt and TUI label.
help = ''
class-attribute
instance-attribute
¶
Longer explanation, rendered next to the input in the TUI; unused in CLI.
choices = ()
class-attribute
instance-attribute
¶
Static (value, label) pairs for kind in {"choice", "multichoice"}.
choices_loader = None
class-attribute
instance-attribute
¶
Runtime resolver for choices that aren't known at import time.
Set this when the option set lives in a sibling wheel (e.g. the
agent roster) and can drift between releases. When set, takes
precedence over choices.
required = False
class-attribute
instance-attribute
¶
Reject empty answers with "<prompt> is required."
transform = None
class-attribute
instance-attribute
¶
Optional normalisation applied before validation (e.g. str.lower).
validate = None
class-attribute
instance-attribute
¶
Optional validator returning an error string or None when accepted.
placeholder = ''
class-attribute
instance-attribute
¶
Hint string, rendered inside the Textual Input; unused in CLI.
default_visible = False
class-attribute
instance-attribute
¶
When True, CLI prompt shows "(optional)" to telegraph "Enter is fine".
resolve_choices()
¶
Return the effective option list — runtime loader wins over static.
Called by both presenters whenever they need to render or validate
a choice / multichoice question. The loader is expected
to be cheap; the executor's roster lookup is itself lru_cache'd
so repeated calls are free.
Source code in src/terok/lib/domain/wizards/new_project.py
discover_projects()
¶
Load every project on disk, splitting successes from config-level failures.
The broken list lets the TUI render damaged projects alongside healthy
ones (issue #565) — silently hiding them turns "project vanished" into
a mystery. _parse_project_yaml wraps every config error (bad YAML,
schema drift, filesystem issues) in SystemExit with a human-readable
message; anything else propagates as a genuine bug.
Source code in src/terok/lib/core/projects.py
load_project(project_id)
¶
Load and return a fully resolved ProjectConfig from project_id.
Source code in src/terok/lib/core/projects.py
require_project_exists(project_id)
¶
Raise SystemExit unless project_id names a known project.
Cheap stat-based check — no YAML parse, no pydantic validation. Use
this in CLI entry points that want to fail before any user-visible
side effect (interactive prompt, status print, image build offer).
The downstream load_project
call still catches malformed YAML.
Source code in src/terok/lib/core/projects.py
set_project_image_agents(project_id, selection)
¶
Write selection into the project's project.yml under image.agents.
Caller validates selection up-front; on success returns the project.yml path written.
Source code in src/terok/lib/core/projects.py
cleanup_images(dry_run=False)
¶
Find and remove orphaned terok images in one shot.
Thin convenience over find_orphaned_images
+ remove_images for callers that
don't need to inspect the list first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dry_run
|
bool
|
If True, only report what would be removed without removing. |
False
|
Returns:
| Type | Description |
|---|---|
CleanupResult
|
CleanupResult with lists of removed and failed image display names. |
Source code in src/terok/lib/domain/image_cleanup.py
find_orphaned_images()
¶
Find terok images that are orphaned and safe to remove.
Orphaned images include:
- Dangling images (<none>:<none>) from terok layer rebuilds
- L2 project images whose project no longer exists in the config
Source code in src/terok/lib/domain/image_cleanup.py
list_images(project_id=None)
¶
List terok-managed images, optionally filtered by project.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_id
|
str | None
|
If given, only show images for this project. |
None
|
Returns:
| Type | Description |
|---|---|
list[ImageInfo]
|
List of ImageInfo objects for matching images. |
Source code in src/terok/lib/domain/image_cleanup.py
remove_images(images, *, dry_run=False)
¶
Remove a pre-computed set of images (or just report under dry_run).
Split out from cleanup_images
so the CLI can present the orphan list, prompt for confirmation, and only
then act — without paying the discovery cost twice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
Iterable[ImageInfo]
|
The images to remove. Iterated once. |
required |
dry_run
|
bool
|
If True, only report names without invoking the runtime. |
False
|
Returns:
| Type | Description |
|---|---|
CleanupResult
|
CleanupResult with lists of removed and failed image display names. |
Source code in src/terok/lib/domain/image_cleanup.py
execute_panic(*, stop_containers=False)
¶
Execute the full panic sequence.
Discovers every running container, then raises shields, stops vault and gate — all in parallel. If stop_containers, also kills the containers afterwards (SIGKILL; they are not removed).
Source code in src/terok/lib/domain/panic.py
format_panic_report(result)
¶
Format a human-readable summary of the panic result.
Source code in src/terok/lib/domain/panic.py
panic_stop_containers()
¶
Discover and SIGKILL all running containers (Phase 2 standalone).
delete_project(project_id)
¶
Delete a project and all its associated data.
Removes task workspaces, task metadata, build artifacts, SSH credentials, the git gate (if not shared with other projects), and the project config directory.
Source code in src/terok/lib/domain/project.py
derive_project(source_id, new_id)
¶
Copy source_id's gate mirror and vault SSH assignments under new_id.
Source code in src/terok/lib/domain/project.py
find_projects_sharing_gate(gate_path, exclude_project=None)
¶
Find all projects configured to use the same gate path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gate_path
|
Path
|
The gate path to check for |
required |
exclude_project
|
str | None
|
Project ID to exclude from results (usually the current project) |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple[str, str | None]]
|
List of (project_id, upstream_url) tuples for projects sharing this gate |
Source code in src/terok/lib/domain/project.py
list_projects()
¶
project_image_exists(project_id)
¶
summarize_ssh_init(result)
¶
Render an ssh-init result for the terminal.
Source code in src/terok/lib/domain/ssh.py
render_project_yaml(values)
¶
Render project.yml without writing it — used by the TUI review screen.
Source code in src/terok/lib/domain/wizards/new_project.py
validate_answer(question, raw)
¶
Normalise and validate a raw answer for question.
Returns (value, error_or_None) — the normalised value and an
error message if the answer was rejected. Both presenters call
this so validation semantics stay identical regardless of UI.
Normalisation, in order:
- Strip surrounding whitespace (copy-paste leftovers, accidental trailing spaces). All-whitespace input is indistinguishable from empty for the required/optional check.
- Apply
question.transformif set (e.g.str.lower). - Enforce the required flag against the final value.
- For
kind="choice", the value must be one of the declared slugs — defensive against presenter bugs that might submit a label, index, or free-form typo. - Run
question.validatefor field-specific rules.
Source code in src/terok/lib/domain/wizards/new_project.py
write_project_yaml(project_id, rendered, *, overwrite=False)
¶
Write rendered YAML to <user_projects_dir>/<project_id>/project.yml.
The TUI reviews YAML in a TextArea before writing, so this is the
write half of generate_config — kept separate so the TUI can
pass tweaked content without re-rendering the template.