Skip to content

kinds

tit.jobs.kinds

Job kind -> runner command line (docs/dev/ARCHITECTURE.md §6, the job engine).

Every "module" kind runs simnibs_python -m <module> <spec_path> — the existing __main__.py contract every runner already implements or will implement (B4). tools is different: it doesn't take a spec.json at all, it runs an arbitrary module with explicit args from the config -- it is therefore allowlisted to tit.tools.* modules only (ra_14 finding #2): any importable module (-m http.server, -m pip, an attacker's own package on PYTHONPATH) would otherwise be remote code execution for any token holder, and in the container that's host root via the mounted docker.sock.

The allowlist closes which module runs; :func:check_tool_args closes what it can be told to touch (RUN-06). Every path-shaped config.args value -- a positional, an option's next argument, --opt=value, or a value attached to a short option (-o/out.stl) -- must resolve inside the manager's project root, checked when the argv is built, before the job is spawned and therefore before any file is created. A tools job whose args mention a path but whose manager has no project root bound is refused rather than trusted.

KindError

Bases: ValueError

Raised for an unknown kind, or a known kind whose runner module isn't wired up yet.

command_for

command_for(kind: str, config: dict[str, Any], spec_path: str, *, project_dir: str | None = None) -> list[str]

The argv to exec for one job. Raises :class:KindError for anything it can't build.

project_dir is the root a tools job's path arguments are jailed to (:func:check_tool_args); :class:tit.jobs.manager.JobManager binds its own.

Source code in tit/jobs/kinds.py
def command_for(
    kind: str,
    config: dict[str, Any],
    spec_path: str,
    *,
    project_dir: str | None = None,
) -> list[str]:
    """The argv to exec for one job. Raises :class:`KindError` for anything it can't build.

    *project_dir* is the root a ``tools`` job's path arguments are jailed to
    (:func:`check_tool_args`); :class:`tit.jobs.manager.JobManager` binds its own.
    """
    config = config or {}

    if kind in MODULE_FOR_KIND:
        module = MODULE_FOR_KIND[kind]
        if not module_exists(module):
            raise KindError(
                f"kind '{kind}' maps to '{module}', which is not importable in this "
                "environment; the TI-Toolbox install is incomplete or damaged."
            )
        return [PYTHON_INTERPRETER, "-m", module, spec_path]

    if kind == "tools":
        module = config.get("module")
        if not module or not isinstance(module, str):
            raise KindError(
                "tools job needs a non-empty config.module (dotted module path)"
            )
        if _resolve_tool_module_path(module) is None:
            raise KindError(
                f"tools job: module {module!r} is not an allowed tit.tools module "
                f"(must be '{TOOLS_MODULE_PREFIX}<name>' and resolve to a file inside "
                f"{TOOLS_DIR})"
            )
        args = _string_list(config.get("args", []))
        check_tool_args(args, project_dir)
        return [PYTHON_INTERPRETER, "-m", module, *args]

    raise KindError(f"unknown job kind: {kind!r}")

check_tool_args

check_tool_args(args: list[str], project_dir: str | None) -> None

Raise :class:KindError unless every argument of a tools job stays in bounds.

RUN-06: config.args used to be forwarded verbatim, so an allowlisted tool could be handed an absolute output path and made to write anywhere the container's user can write. Every value that looks like a path must resolve inside project_dir: positionals (which covers tit.tools.electrode_overlay's three paths), an option's next argument, --opt=value, and every reading of a value attached to a short option.

Source code in tit/jobs/kinds.py
def check_tool_args(args: list[str], project_dir: str | None) -> None:
    """Raise :class:`KindError` unless every argument of a ``tools`` job stays in bounds.

    RUN-06: ``config.args`` used to be forwarded verbatim, so an allowlisted tool could be
    handed an absolute output path and made to write anywhere the container's user can write.
    Every value that looks like a path must resolve inside *project_dir*: positionals (which
    covers ``tit.tools.electrode_overlay``'s three paths), an option's next argument,
    ``--opt=value``, and every reading of a value attached to a short option.
    """
    from tit.paths import is_within

    def check(where: str, value: str) -> None:
        if not _looks_like_a_path(value):
            return
        if not project_dir:
            raise KindError(
                f"tools job: {where} {value!r} looks like a path, and this manager has no "
                f"project directory to jail it to"
            )
        resolved = os.path.join(project_dir, os.path.expanduser(value))
        if not is_within(project_dir, resolved):
            raise KindError(
                f"tools job: {where} {value!r} resolves outside the project directory"
            )

    pending: str | None = None
    for arg in args:
        if arg.startswith("-") and not os.path.isabs(arg):
            option, sep, inline = arg.partition("=")
            if sep:
                check(option, inline)
            if not arg.startswith("--"):
                for value in _attached_values(arg):
                    check(arg, value)
            pending = None if sep else option
            continue
        check(pending or "argument", arg)
        pending = None

may_spawn_docker_siblings

may_spawn_docker_siblings(kind: str | None, config: Any = None) -> bool

Could a job of this kind/config have started a sibling Docker container?

Used to keep cancellation from touching Docker for the overwhelming majority of jobs that never could have spawned one -- an unresponsive daemon then cannot delay their cancel. Conservative on purpose: a pre job whose config does not mention any DWI stage flag at all (an unrecognised or future shape) is treated as if it might have spawned one.

Source code in tit/jobs/kinds.py
def may_spawn_docker_siblings(kind: str | None, config: Any = None) -> bool:
    """Could a job of this *kind*/*config* have started a sibling Docker container?

    Used to keep cancellation from touching Docker for the overwhelming majority of jobs that
    never could have spawned one -- an unresponsive daemon then cannot delay their cancel.
    Conservative on purpose: a ``pre`` job whose config does not mention any DWI stage flag at
    all (an unrecognised or future shape) is treated as if it might have spawned one.
    """
    if kind != "pre":
        return False
    if not isinstance(config, dict):
        return True
    present = [f for f in DOCKER_SIBLING_STAGE_FLAGS if f in config]
    if not present:
        return True
    return any(bool(config[f]) for f in present)