Skip to content

API reference

Auto-generated from docstrings via mkdocstrings.

Conventions

  • Only names exported from the top-level ocx_sdk package (and the one public submodule, ocx_sdk.bootstrap) are public. Everything reachable through any other underscored module path is package-private and may change without notice — pre-1.0, breaking changes ship without shims.
  • Docstrings follow Google style.
  • Raises: sections list every exception a method may raise from the SDK's own hierarchy; lower-layer exceptions are wrapped and preserved on __cause__.
  • Every method mirrors one ocx command; see the command ↔ method map for the full correspondence, and environment & exit codes for the wire-level detail behind the typed surface.

ocx_sdk

Python SDK for OCX.

ocx-sdk drives the ocx binary rather than reimplementing it: ocx owns resolution, verification, and the identifier grammar, and this package gives you typed, CWD-independent handles over the commands it exposes.

from ocx_sdk import Ocx, bootstrap

ocx = Ocx(exe=bootstrap.ensure())
project = ocx.project("/srv/build")
project.pull()
project.exec(["task", "verify"])

This module is the API. Everything listed in __all__ is the stable surface; every other module is underscored and package-private, and the one public submodule is ocx_sdk.bootstrap. Reaching into an underscored path means the next release may move it without notice — pre-1.0, breaking changes ship without shims.

Start at Ocx for the runtime API and bootstrap.ensure for provisioning.

UNSET module-attribute

UNSET: Final = TOKEN

The default for every per-call retry=/timeout=, meaning "not given".

Public so that a wrapper around this SDK can pass a caller's override straight through without having to invent its own three-state sentinel: def deploy(*, retry: MaybeRetry = UNSET): ocx.package.push(..., retry=retry).

MANAGED_CONFIG_DISABLED module-attribute

MANAGED_CONFIG_DISABLED: Final = ''

Wire sentinel that force-disables the managed config tier for a spawn.

MIN_SUPPORTED module-attribute

MIN_SUPPORTED: Final = '0.6.2'

Oldest ocx the SDK accepts; below it the compat gate raises VersionCompatError.

TESTED_OCX_VERSION module-attribute

TESTED_OCX_VERSION: Final = '0.6.2'

The ocx version this SDK's contract tests run against.

ConfigCommands dataclass

ConfigCommands(_runner: _Runner)

The ocx config command group — the corporate managed-config tier.

setup

setup(*, managed_config: str | None = None, dry_run: bool = False, force: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> ConfigSetupReport

Adopt, or clear, the managed-config tier.

Installs no binary and touches no shell profile — the automation-shaped counterpart to self setup --managed-config.

PARAMETER DESCRIPTION
managed_config

An OCI reference to a managed-config artifact. MANAGED_CONFIG_DISABLED clears an existing seed. None falls back to the ambient source, then the existing seed.

TYPE: str | None DEFAULT: None

dry_run

Report the intended actions without writing.

TYPE: bool DEFAULT: False

force

Overwrite a managed fence that carries local edits.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
ConfigSetupReport

The status ocx recorded.

RAISES DESCRIPTION
DirtyRcBlockError

The fence carries local edits and force is unset.

Source code in src/ocx_sdk/_client.py
def setup(
    self,
    *,
    managed_config: str | None = None,
    dry_run: bool = False,
    force: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> ConfigSetupReport:
    """Adopt, or clear, the managed-config tier.

    Installs no binary and touches no shell profile — the
    automation-shaped counterpart to `self setup --managed-config`.

    Args:
        managed_config: An OCI reference to a managed-config artifact.
            `MANAGED_CONFIG_DISABLED` clears an existing seed. `None`
            falls back to the ambient source, then the existing seed.
        dry_run: Report the intended actions without writing.
        force: Overwrite a managed fence that carries local edits.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The status ocx recorded.

    Raises:
        DirtyRcBlockError: The fence carries local edits and `force` is
            unset.
    """
    command = [
        "config",
        "setup",
        *_flag("--managed-config", managed_config),
        *_switch("--dry-run", dry_run),
        *_switch("--force", force),
    ]
    result = self._runner.typed(command, timeout=timeout, retry=retry)
    return ConfigSetupReport.from_json(result.stdout)

update

update(version: str | None = None, *, check_only: bool = False, pause: str | None = None, resume: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> ConfigUpdateReport

Refresh the managed-config snapshot from the registry.

Unlike the project-tier lock --check, this command answers --check with a full payload — status reads checked or check_unavailable.

PARAMETER DESCRIPTION
version

A tag, sha256:<hex>, or tag@sha256:<hex> to sync. None follows the seed's own source.

TYPE: str | None DEFAULT: None

check_only

Report status — drift, pause, pin — without fetching or swapping. ocx's --check.

TYPE: bool DEFAULT: False

pause

Hold the background refresh for a duration such as 4h or 3d, up to seven days.

TYPE: str | None DEFAULT: None

resume

Clear an active pause and refresh immediately.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
ConfigUpdateReport

The tier's status, source, digest, and policy.

Source code in src/ocx_sdk/_client.py
def update(
    self,
    version: str | None = None,
    *,
    check_only: bool = False,
    pause: str | None = None,
    resume: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> ConfigUpdateReport:
    """Refresh the managed-config snapshot from the registry.

    Unlike the project-tier `lock --check`, this command answers `--check`
    with a full payload — `status` reads `checked` or `check_unavailable`.

    Args:
        version: A tag, `sha256:<hex>`, or `tag@sha256:<hex>` to sync.
            `None` follows the seed's own source.
        check_only: Report status — drift, pause, pin — without fetching
            or swapping. ocx's `--check`.
        pause: Hold the background refresh for a duration such as `4h`
            or `3d`, up to seven days.
        resume: Clear an active pause and refresh immediately.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The tier's status, source, digest, and policy.
    """
    command = [
        "config",
        "update",
        *_switch("--check", check_only),
        *_flag("--pause", pause),
        *_switch("--resume", resume),
    ]
    positionals = () if version is None else (version,)
    result = self._runner.typed(command, positionals, timeout=timeout, retry=retry)
    return ConfigUpdateReport.from_json(result.stdout)

Ocx

Ocx(exe: str | Path | None = None, *, config: OcxConfig | None = None, host_env: HostEnv | None = None, on_log: Callable[[str], None] | None = None)

A handle on one ocx binary.

Construction resolves the binary once — the explicit exe, then OCX_SDK_EXE, then PATH, then ocx's own install symlink — and pins the result for the handle's lifetime. Every call composes its own argv and child environment, and the handle exposes no mutators, so it is safe to share across threads, tasks, and event loops.

The first typed call probes ocx version and refuses a binary older than MIN_SUPPORTED; newer than TESTED_OCX_VERSION is expected and only noted at DEBUG.

A long-lived handle keeps running the binary it resolved, so a consumer shaped like a daemon should reconstruct Ocx() periodically to pick up what an ocx self update replaced underneath it.

Example
from ocx_sdk import Ocx, OcxConfig

ocx = Ocx(config=OcxConfig(offline=True))
project = ocx.project("/srv/build")
project.pull()
result = project.exec(["task", "verify"])
ATTRIBUTE DESCRIPTION
exe

The resolved binary this handle runs.

TYPE: Path

session_config

The OcxConfig every call spawns under.

TYPE: OcxConfig

PARAMETER DESCRIPTION
exe

An explicit binary path. None discovers one.

TYPE: str | Path | None DEFAULT: None

config

Session configuration. None uses the defaults.

TYPE: OcxConfig | None DEFAULT: None

host_env

The environment children inherit from. None snapshots the ambient one at construction. Named apart from the env= on run/env()/inspect(), which takes [env] entries for one call and is a different thing entirely.

TYPE: HostEnv | None DEFAULT: None

on_log

Called with each redacted stderr line from the pump thread, so it must be thread-safe. Rejected on async paths.

TYPE: Callable[[str], None] | None DEFAULT: None

RAISES DESCRIPTION
OcxNotFoundError

No binary was found; the message names bootstrap.ensure() as the fix.

Source code in src/ocx_sdk/_client.py
def __init__(
    self,
    exe: str | Path | None = None,
    *,
    config: OcxConfig | None = None,
    host_env: HostEnv | None = None,
    on_log: Callable[[str], None] | None = None,
) -> None:
    """Resolve a binary and pin it to this handle.

    Args:
        exe: An explicit binary path. `None` discovers one.
        config: Session configuration. `None` uses the defaults.
        host_env: The environment children inherit from. `None` snapshots
            the ambient one at construction. Named apart from the `env=`
            on `run`/`env()`/`inspect()`, which takes `[env]` entries for
            one call and is a different thing entirely.
        on_log: Called with each redacted stderr line from the pump
            thread, so it must be thread-safe. Rejected on async paths.

    Raises:
        OcxNotFoundError: No binary was found; the message names
            `bootstrap.ensure()` as the fix.
    """
    host = HostEnv.ambient() if host_env is None else host_env
    self._runner = _Runner(
        _bootstrap.discover(exe, host).resolve(),
        OcxConfig() if config is None else config,
        host,
        on_log,
        _CompatCell(),
    )

exe property

exe: Path

The resolved binary path, fixed at construction.

session_config property

session_config: OcxConfig

The OcxConfig this handle spawns under.

Read-only, and the config itself is frozen: derive a variant with with_config(...) rather than trying to write through this. Not spelled config because ocx.config is the ocx config command group, and command alignment wins over the shorter name.

package property

package: PackageCommands

The ocx package command group — machine tier, no project path.

config property

The ocx config command group — the managed-config tier.

patch property

The ocx patch command group.

with_config

with_config(**overrides: Unpack[ConfigOverrides]) -> Ocx

Derive a handle with some configuration fields replaced.

The derived handle shares this one's binary, host environment, on_log, and compatibility memo — only the configuration differs. Changing any of the others means constructing a fresh Ocx.

PARAMETER DESCRIPTION
**overrides

OcxConfig field names and their new values, as ConfigOverrides spells them.

TYPE: Unpack[ConfigOverrides] DEFAULT: {}

RETURNS DESCRIPTION
Ocx

The derived handle.

RAISES DESCRIPTION
TypeError

An override names no OcxConfig field. A type checker catches that first; the runtime guard is for callers who build the keywords dynamically.

Source code in src/ocx_sdk/_client.py
def with_config(self, **overrides: Unpack[ConfigOverrides]) -> Ocx:
    """Derive a handle with some configuration fields replaced.

    The derived handle shares this one's binary, host environment,
    `on_log`, and compatibility memo — only the configuration differs.
    Changing any of the others means constructing a fresh `Ocx`.

    Args:
        **overrides: `OcxConfig` field names and their new values, as
            `ConfigOverrides` spells them.

    Returns:
        The derived handle.

    Raises:
        TypeError: An override names no `OcxConfig` field. A type checker
            catches that first; the runtime guard is for callers who
            build the keywords dynamically.
    """
    return Ocx._derive(self._runner.derive(**overrides))

project

project(path: str | Path) -> Project

Return a project-tier handle rooted at path.

The path is made absolute immediately, so the handle keeps targeting the same project no matter what the working directory does later.

ocx's --project names the project file, the way Cargo's --manifest-path does, and accepts any filename. A directory is the obvious thing to pass and the common case, so it is resolved to the ocx.toml inside it.

A path that does not exist yet — which is every path init() is called on — is read by its name: a .toml suffix means the file, anything else means the directory. Guessing "file" there would make project('/srv/build').init() write /srv/ocx.toml, one level above where the caller pointed.

PARAMETER DESCRIPTION
path

The project directory, or the project file directly.

TYPE: str | Path

RETURNS DESCRIPTION
Project

A handle whose every call carries --project <file>.

Source code in src/ocx_sdk/_client.py
def project(self, path: str | Path) -> Project:
    """Return a project-tier handle rooted at `path`.

    The path is made absolute immediately, so the handle keeps targeting
    the same project no matter what the working directory does later.

    ocx's `--project` names the project *file*, the way Cargo's
    `--manifest-path` does, and accepts any filename. A directory is the
    obvious thing to pass and the common case, so it is resolved to the
    `ocx.toml` inside it.

    A path that does not exist yet — which is every path `init()` is
    called on — is read by its name: a `.toml` suffix means the file,
    anything else means the directory. Guessing "file" there would make
    `project('/srv/build').init()` write `/srv/ocx.toml`, one level above
    where the caller pointed.

    Args:
        path: The project directory, or the project file directly.

    Returns:
        A handle whose every call carries `--project <file>`.
    """
    resolved = Path(path).resolve()
    names_the_file = resolved.suffix == _PROJECT_SUFFIX or resolved.is_file()
    return Project(self._runner, resolved if names_the_file else resolved / _PROJECT_FILE)

version

version(*, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> str

Return the binary's version, as ocx version prints it.

Reads the plain output — a bare semver line — which is ocx's documented stable script contract and the recorded exception to the --format json pinning rule. This call is the compatibility probe itself, so it never triggers the gate and never raises VersionCompatError: asking an unsupported binary what it is has to work. The richer JSON envelope is available through VersionInfo.from_json(ocx.invoke(["--format", "json", "version"]).stdout).

PARAMETER DESCRIPTION
timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
str

The version string, whitespace stripped.

Source code in src/ocx_sdk/_client.py
def version(self, *, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> str:
    """Return the binary's version, as `ocx version` prints it.

    Reads the plain output — a bare semver line — which is ocx's
    documented stable script contract and the recorded exception to the
    `--format json` pinning rule. This call is the compatibility probe
    itself, so it never triggers the gate and never raises
    `VersionCompatError`: asking an unsupported binary what it is has to
    work. The richer JSON envelope is available through
    `VersionInfo.from_json(ocx.invoke(["--format", "json",
    "version"]).stdout)`.

    Args:
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The version string, whitespace stripped.
    """
    return self._runner.probe(timeout=timeout, retry=retry)

about

about(*, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> AboutInfo

Report what this ocx build is and where it keeps its state.

PARAMETER DESCRIPTION
timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
AboutInfo

The parsed ocx about payload.

Source code in src/ocx_sdk/_client.py
def about(self, *, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> AboutInfo:
    """Report what this ocx build is and where it keeps its state.

    Args:
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The parsed `ocx about` payload.
    """
    return AboutInfo.from_json(self._runner.typed(("about",), timeout=timeout, retry=retry).stdout)

login

login(registry: str | None = None, *, username: str, token: str, allow_insecure_store: bool = False, verify: bool = True, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> LoginResult

Authenticate to a registry and let ocx persist the credentials.

The token travels on stdin through --password-stdin, never in argv, and is redacted from logs and errors for the duration of the call.

Retries are off by default: a timed-out login exits 75, and re-sending credentials on a timeout is the auth-retry mistake the policy exists to prevent. Pass retry= explicitly to override.

PARAMETER DESCRIPTION
registry

The registry hostname. None falls back to ocx's OCX_DEFAULT_REGISTRY.

TYPE: str | None DEFAULT: None

username

The account name. Required — the SDK never prompts.

TYPE: str

token

The password or token, written to ocx's stdin.

TYPE: str

allow_insecure_store

Permit the plaintext auths fallback when no credential helper is configured.

TYPE: bool DEFAULT: False

verify

Check the credentials against the registry before storing them.

TYPE: bool DEFAULT: True

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
LoginResult

The registry and username ocx recorded.

Source code in src/ocx_sdk/_client.py
def login(
    self,
    registry: str | None = None,
    *,
    username: str,
    token: str,
    allow_insecure_store: bool = False,
    verify: bool = True,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> LoginResult:
    """Authenticate to a registry and let ocx persist the credentials.

    The token travels on stdin through `--password-stdin`, never in argv,
    and is redacted from logs and errors for the duration of the call.

    Retries are off by default: a timed-out login exits 75, and
    re-sending credentials on a timeout is the auth-retry mistake the
    policy exists to prevent. Pass `retry=` explicitly to override.

    Args:
        registry: The registry hostname. `None` falls back to ocx's
            `OCX_DEFAULT_REGISTRY`.
        username: The account name. Required — the SDK never prompts.
        token: The password or token, written to ocx's stdin.
        allow_insecure_store: Permit the plaintext `auths` fallback when
            no credential helper is configured.
        verify: Check the credentials against the registry before
            storing them.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries.

    Returns:
        The registry and username ocx recorded.
    """
    command = [
        "login",
        "--username",
        username,
        "--password-stdin",
        *_switch("--allow-insecure-store", allow_insecure_store),
        *_switch("--no-verify", not verify),
    ]
    result = self._runner.typed(
        command,
        () if registry is None else (registry,),
        input=token,
        timeout=timeout,
        retry=retry,
        mutating=True,
    )
    return LoginResult.from_json(result.stdout)

logout

logout(registry: str | None = None, *, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> LogoutResult

Drop the stored credentials for a registry.

PARAMETER DESCRIPTION
registry

The registry hostname. None falls back to ocx's OCX_DEFAULT_REGISTRY.

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
LogoutResult

The registry ocx cleared. Clearing an absent entry is not an

LogoutResult

error.

Source code in src/ocx_sdk/_client.py
def logout(
    self,
    registry: str | None = None,
    *,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> LogoutResult:
    """Drop the stored credentials for a registry.

    Args:
        registry: The registry hostname. `None` falls back to ocx's
            `OCX_DEFAULT_REGISTRY`.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The registry ocx cleared. Clearing an absent entry is not an
        error.
    """
    positionals = () if registry is None else (registry,)
    result = self._runner.typed(("logout",), positionals, timeout=timeout, retry=retry)
    return LogoutResult.from_json(result.stdout)

clean

clean(*, dry_run: bool = False, force: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[CleanEntry, ...]

Remove unreferenced objects from the local object store.

mutating=not dry_run (D5), as copy: the preview writes nothing. The rows are the read-only view of what the store holds loose — the kind: "consent" entries among them are the stamps OcxConfig.consent governs, so a preview says which projects a real run would deactivate.

PARAMETER DESCRIPTION
dry_run

Report what would be removed and remove nothing.

TYPE: bool DEFAULT: False

force

Ignore the per-user project registry and collect every unreferenced package, including ones a registered ocx.lock still pins. Live install symlinks are always honoured regardless.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's, which D5 resolves to no retries unless dry_run.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[CleanEntry, ...]

One row per removal, or intended removal.

Source code in src/ocx_sdk/_client.py
def clean(
    self,
    *,
    dry_run: bool = False,
    force: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[CleanEntry, ...]:
    """Remove unreferenced objects from the local object store.

    `mutating=not dry_run` (D5), as `copy`: the preview writes nothing.
    The rows are the read-only view of what the store holds loose — the
    `kind: "consent"` entries among them are the stamps `OcxConfig.consent`
    governs, so a preview says which projects a real run would deactivate.

    Args:
        dry_run: Report what would be removed and remove nothing.
        force: Ignore the per-user project registry and collect every
            unreferenced package, including ones a registered
            `ocx.lock` still pins. Live install symlinks are always
            honoured regardless.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's,
            which D5 resolves to no retries unless `dry_run`.

    Returns:
        One row per removal, or intended removal.
    """
    command = ["clean", *_switch("--dry-run", dry_run), *_switch("--force", force)]
    result = self._runner.typed(command, timeout=timeout, retry=retry, mutating=not dry_run)
    return parse_clean(result.stdout)

invoke

invoke(argv: Sequence[str], *, check: bool = True, capture: bool = True, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CommandResult

Run an arbitrary ocx command line — the raw escape hatch.

The configured globals and the composed child environment still apply; presentation does not, so ask for --format json yourself if you intend to parse the output. Commands reached this way are possible but unsupported: nothing pins their shape, and the compatibility gate does not run.

PARAMETER DESCRIPTION
argv

The command and its arguments, without the binary.

TYPE: Sequence[str]

check

Raise on a non-zero exit instead of returning it.

TYPE: bool DEFAULT: True

capture

Pipe and capture both streams.

TYPE: bool DEFAULT: True

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The exit code and captured output.

RAISES DESCRIPTION
OcxProcessError

A non-zero exit under check.

OcxTimeoutError

The timeout expired.

Source code in src/ocx_sdk/_client.py
def invoke(
    self,
    argv: Sequence[str],
    *,
    check: bool = True,
    capture: bool = True,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CommandResult:
    """Run an arbitrary ocx command line — the raw escape hatch.

    The configured globals and the composed child environment still
    apply; presentation does not, so ask for `--format json` yourself if
    you intend to parse the output. Commands reached this way are
    possible but unsupported: nothing pins their shape, and the
    compatibility gate does not run.

    Args:
        argv: The command and its arguments, without the binary.
        check: Raise on a non-zero exit instead of returning it.
        capture: Pipe and capture both streams.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The exit code and captured output.

    Raises:
        OcxProcessError: A non-zero exit under `check`.
        OcxTimeoutError: The timeout expired.
    """
    composed = self._runner.compose(argv, presentation=_RAW)
    return self._runner.finish(composed, capture=capture, check=check, timeout=timeout, retry=retry)

invoke_async async

invoke_async(argv: Sequence[str], *, check: bool = True, capture: bool = True, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CommandResult

Run an arbitrary ocx command line on the event loop.

The async twin of invoke. Cancelling the awaiting task terminates the child before the CancelledError propagates.

PARAMETER DESCRIPTION
argv

The command and its arguments, without the binary.

TYPE: Sequence[str]

check

Raise on a non-zero exit instead of returning it.

TYPE: bool DEFAULT: True

capture

Pipe and capture both streams.

TYPE: bool DEFAULT: True

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The exit code and captured output.

RAISES DESCRIPTION
ValueError

The handle carries an on_log callback, which v0.1 refuses on async paths.

OcxProcessError

A non-zero exit under check.

OcxTimeoutError

The timeout expired.

Source code in src/ocx_sdk/_client.py
async def invoke_async(
    self,
    argv: Sequence[str],
    *,
    check: bool = True,
    capture: bool = True,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CommandResult:
    """Run an arbitrary ocx command line on the event loop.

    The async twin of `invoke`. Cancelling the awaiting task terminates
    the child before the `CancelledError` propagates.

    Args:
        argv: The command and its arguments, without the binary.
        check: Raise on a non-zero exit instead of returning it.
        capture: Pipe and capture both streams.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The exit code and captured output.

    Raises:
        ValueError: The handle carries an `on_log` callback, which v0.1
            refuses on async paths.
        OcxProcessError: A non-zero exit under `check`.
        OcxTimeoutError: The timeout expired.
    """
    composed = self._runner.compose(argv, presentation=_RAW)
    return await self._runner.finish_async(composed, capture=capture, check=check, timeout=timeout, retry=retry)

spawn

spawn(argv: Sequence[str], **popen_kw: Any) -> Popen[Any]

Start an arbitrary ocx command line and return the live Popen.

No timeout and no pumps: waiting, draining pipes, and killing belong to the caller, exactly as with a bare Popen.

PARAMETER DESCRIPTION
argv

The command and its arguments, without the binary.

TYPE: Sequence[str]

**popen_kw

Forwarded to Popen. args, shell, and executable are rejected; the environment is SDK-composed.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Popen[Any]

The running child.

RAISES DESCRIPTION
ValueError

args, shell, or executable was passed.

TypeError

env was passed. It is the process layer's own parameter, so it arrives as a duplicate argument rather than a rejected one. Put what the child needs into OcxConfig or the HostEnv this handle was built with.

Source code in src/ocx_sdk/_client.py
def spawn(self, argv: Sequence[str], **popen_kw: Any) -> subprocess.Popen[Any]:
    """Start an arbitrary ocx command line and return the live `Popen`.

    No timeout and no pumps: waiting, draining pipes, and killing belong
    to the caller, exactly as with a bare `Popen`.

    Args:
        argv: The command and its arguments, without the binary.
        **popen_kw: Forwarded to `Popen`. `args`, `shell`, and
            `executable` are rejected; the environment is SDK-composed.

    Returns:
        The running child.

    Raises:
        ValueError: `args`, `shell`, or `executable` was passed.
        TypeError: `env` was passed. It is the process layer's own
            parameter, so it arrives as a duplicate argument rather than
            a rejected one. Put what the child needs into `OcxConfig` or
            the `HostEnv` this handle was built with.
    """
    return self._runner.launch(self._runner.compose(argv, presentation=_RAW), popen_kw)

spawn_async async

spawn_async(argv: Sequence[str], **popen_kw: Any) -> Process

Start an arbitrary ocx command line on the event loop.

PARAMETER DESCRIPTION
argv

The command and its arguments, without the binary.

TYPE: Sequence[str]

**popen_kw

Forwarded to the subprocess factory. args, shell, and executable are rejected.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Process

The running child.

RAISES DESCRIPTION
ValueError

args, shell, or executable was passed.

TypeError

env was passed — see spawn.

Source code in src/ocx_sdk/_client.py
async def spawn_async(self, argv: Sequence[str], **popen_kw: Any) -> asyncio.subprocess.Process:
    """Start an arbitrary ocx command line on the event loop.

    Args:
        argv: The command and its arguments, without the binary.
        **popen_kw: Forwarded to the subprocess factory. `args`, `shell`,
            and `executable` are rejected.

    Returns:
        The running child.

    Raises:
        ValueError: `args`, `shell`, or `executable` was passed.
        TypeError: `env` was passed — see `spawn`.
    """
    return await self._runner.launch_async(self._runner.compose(argv, presentation=_RAW), popen_kw)

PackageCommands dataclass

PackageCommands(_runner: _Runner)

The ocx package command group — machine tier.

Package operations act on the $OCX_HOME store and its candidate and current symlinks. They take no project path and are CWD-independent by construction; a path appears only where the CLI itself takes one.

Every method is multi-identifier native, mirroring the CLI's PKG... with one shared resolution.

install

install(*refs: PackageLike, platform: str | None = None, select: bool = False, verify: bool | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> InstallReport

Install packages into the store.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

select

Also make each installed version current.

TYPE: bool DEFAULT: False

verify

Verify each package's Sigstore signature before installing. None leaves ocx's default, which is on. The gate fires only where a [[trust.policy]] covers the package, so True against an uncovered package is a documented no-op rather than enforcement — it does not make an unsigned package fail. False also beats an ambient OCX_NO_VERIFY, which the SDK neutralizes on every spawn.

TYPE: bool | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
InstallReport

The installed packages, keyed by the identifier as given.

RAISES DESCRIPTION
OcxProcessError

ocx 0.6 verifies a covered package's signature before installing, so this call can fail where the identical call against a 0.5.x binary succeeded — no code change on the caller's side, only a newer binary. The exit codes, and why none of them retry, are in the guide's "Errors & credentials" section on verification.

Source code in src/ocx_sdk/_client.py
def install(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    select: bool = False,
    verify: bool | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> InstallReport:
    """Install packages into the store.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        select: Also make each installed version current.
        verify: Verify each package's Sigstore signature before
            installing. `None` leaves ocx's default, which is on. The
            gate fires only where a `[[trust.policy]]` covers the
            package, so `True` against an uncovered package is a
            documented no-op rather than enforcement — it does not make
            an unsigned package fail. `False` also beats an ambient
            `OCX_NO_VERIFY`, which the SDK neutralizes on every spawn.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The installed packages, keyed by the identifier as given.

    Raises:
        OcxProcessError: ocx 0.6 verifies a covered package's signature
            before installing, so this call can fail where the identical
            call against a 0.5.x binary succeeded — no code change on the
            caller's side, only a newer binary. The exit codes, and why
            none of them retry, are in the guide's "Errors & credentials"
            section on verification.
    """
    command = [
        "package",
        "install",
        *_switch("--select", select),
        *_flag("--platform", platform),
        *_toggle("--verify", "--no-verify", verify),
    ]
    return InstallReport.from_json(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

select

select(*refs: PackageLike, platform: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> InstallReport

Make an installed version the current one.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
InstallReport

The selected packages, whose path names the current symlink.

Source code in src/ocx_sdk/_client.py
def select(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> InstallReport:
    """Make an installed version the current one.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The selected packages, whose `path` names the `current` symlink.
    """
    command = ["package", "select", *_flag("--platform", platform)]
    return InstallReport.from_json(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

uninstall

uninstall(*refs: PackageLike, deselect: bool = False, purge: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[RemovalResult, ...]

Remove installed candidates.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

deselect

Also drop the current symlink.

TYPE: bool DEFAULT: False

purge

Delete the object from the store once nothing references it.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[RemovalResult, ...]

One row per removal.

Source code in src/ocx_sdk/_client.py
def uninstall(
    self,
    *refs: PackageLike,
    deselect: bool = False,
    purge: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[RemovalResult, ...]:
    """Remove installed candidates.

    Args:
        *refs: Package identifiers.
        deselect: Also drop the `current` symlink.
        purge: Delete the object from the store once nothing references
            it.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        One row per removal.
    """
    command = ["package", "uninstall", *_switch("--deselect", deselect), *_switch("--purge", purge)]
    return parse_removals(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

deselect

deselect(*refs: PackageLike, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[RemovalResult, ...]

Drop the current symlink, leaving the candidates installed.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[RemovalResult, ...]

One row per removal.

Source code in src/ocx_sdk/_client.py
def deselect(
    self,
    *refs: PackageLike,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[RemovalResult, ...]:
    """Drop the `current` symlink, leaving the candidates installed.

    Args:
        *refs: Package identifiers.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        One row per removal.
    """
    command = ["package", "deselect"]
    return parse_removals(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

env

env(*refs: PackageLike, platform: str | None = None, env: Mapping[str, EnvValue] | None = None, resolve: Resolve | None = None, private: bool = False, lazy_mode: LazyMode | None = None, show_patches: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> EnvReport

Report the environment a set of packages composes.

Returns the same envelope as the project-tier env, and carries the same host snapshot for a hermetic compose().

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

resolve

Which store symlink to resolve through — 'candidate' or 'current'. None leaves the choice to ocx. One value, not two booleans: ocx's --candidate and --current name one link each, so a pair of flags could ask for both.

TYPE: Resolve | None DEFAULT: None

private

Compose the private surface instead of the interface one — ocx's --self.

TYPE: bool DEFAULT: False

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

show_patches

Include patch-contributed entries.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
EnvReport

The typed entries plus the full envelope.

Source code in src/ocx_sdk/_client.py
def env(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    env: Mapping[str, EnvValue] | None = None,
    resolve: Resolve | None = None,
    private: bool = False,
    lazy_mode: LazyMode | None = None,
    show_patches: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> EnvReport:
    """Report the environment a set of packages composes.

    Returns the same envelope as the project-tier `env`, and carries the
    same host snapshot for a hermetic `compose()`.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        env: Extra `[env]` entries for this call.
        resolve: Which store symlink to resolve through — `'candidate'`
            or `'current'`. `None` leaves the choice to ocx. One value,
            not two booleans: ocx's `--candidate` and `--current` name
            one link each, so a pair of flags could ask for both.
        private: Compose the private surface instead of the interface
            one — ocx's `--self`.
        lazy_mode: When content downloads — now, or on first use.
        show_patches: Include patch-contributed entries.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The typed entries plus the full envelope.
    """
    command = [
        "package",
        "env",
        *_resolve_flag(resolve),
        *_switch("--self", private),
        *_flag("--platform", platform),
        *_flag("--lazy-mode", lazy_mode),
        *_switch("--show-patches", show_patches),
        *_env_flags(env),
    ]
    raw = self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout
    return EnvReport.from_json(raw, base=self._runner.host.source)

exec

exec(refs: Sequence[PackageLike], argv: Sequence[str], *, platform: str | None = None, clean: bool = False, private: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, records_dir: str | Path | None = None, records_name: str | None = None, capture: bool = True, check: bool = True, timeout: MaybeTimeout = UNSET) -> CommandResult

Run a command in a set of packages' composed environment.

Two positional groups, mirroring the CLI's PKG... -- CMD.... The child's exit code comes back byte for byte; child processes are never retried.

PARAMETER DESCRIPTION
refs

Package identifiers to compose.

TYPE: Sequence[PackageLike]

argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

private

Compose the private surface — ocx's --self.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

records_dir

Write an execution record — the resolved closure and executable — under this directory. None leaves it to [records] dir and OCX_RECORDS_DIR; with none of the three, no record is written.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template, over {time}, {host}, {pid} and {rand}. None leaves it to [records] name and OCX_RECORDS_NAME.

TYPE: str | None DEFAULT: None

capture

Pipe and capture both streams.

TYPE: bool DEFAULT: True

check

Raise on a non-zero child exit instead of returning it.

TYPE: bool DEFAULT: True

timeout

Seconds for the whole run. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The child's exit code and, under capture, its output.

RAISES DESCRIPTION
ValueError

argv is empty.

OcxProcessError

The child exited non-zero under check.

OcxTimeoutError

The timeout expired.

Source code in src/ocx_sdk/_client.py
def exec(
    self,
    refs: Sequence[PackageLike],
    argv: Sequence[str],
    *,
    platform: str | None = None,
    clean: bool = False,
    private: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    capture: bool = True,
    check: bool = True,
    timeout: MaybeTimeout = UNSET,
) -> CommandResult:
    """Run a command in a set of packages' composed environment.

    Two positional groups, mirroring the CLI's `PKG... -- CMD...`. The
    child's exit code comes back byte for byte; child processes are
    never retried.

    Args:
        refs: Package identifiers to compose.
        argv: The command and its arguments. Must not be empty.
        platform: The platform to resolve against.
        clean: Strip the ambient parent environment before composing.
        private: Compose the private surface — ocx's `--self`.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        records_dir: Write an execution record — the resolved closure
            and executable — under this directory. `None` leaves it to
            `[records] dir` and `OCX_RECORDS_DIR`; with none of the
            three, no record is written.
        records_name: The record's filename template, over `{time}`,
            `{host}`, `{pid}` and `{rand}`. `None` leaves it to
            `[records] name` and `OCX_RECORDS_NAME`.
        capture: Pipe and capture both streams.
        check: Raise on a non-zero child exit instead of returning it.
        timeout: Seconds for the whole run. Omitted takes the config's.

    Returns:
        The child's exit code and, under `capture`, its output.

    Raises:
        ValueError: `argv` is empty.
        OcxProcessError: The child exited non-zero under `check`.
        OcxTimeoutError: The timeout expired.
    """
    composed = self._exec_argv(refs, argv, platform, clean, private, env, lazy_mode, records_dir, records_name)
    self._runner.gate()
    return self._runner.finish(composed, capture=capture, check=check, timeout=timeout, retry=None)

exec_async async

exec_async(refs: Sequence[PackageLike], argv: Sequence[str], *, platform: str | None = None, clean: bool = False, private: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, records_dir: str | Path | None = None, records_name: str | None = None, capture: bool = True, check: bool = True, timeout: MaybeTimeout = UNSET) -> CommandResult

Run a command in a packages' environment, on the event loop.

PARAMETER DESCRIPTION
refs

Package identifiers to compose.

TYPE: Sequence[PackageLike]

argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

private

Compose the private surface — ocx's --self.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

records_dir

Write an execution record — the resolved closure and executable — under this directory. None leaves it to [records] dir and OCX_RECORDS_DIR; with none of the three, no record is written.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template, over {time}, {host}, {pid} and {rand}. None leaves it to [records] name and OCX_RECORDS_NAME.

TYPE: str | None DEFAULT: None

capture

Pipe and capture both streams.

TYPE: bool DEFAULT: True

check

Raise on a non-zero child exit instead of returning it.

TYPE: bool DEFAULT: True

timeout

Seconds for the whole run. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The child's exit code and, under capture, its output.

RAISES DESCRIPTION
ValueError

argv is empty, or the handle carries an on_log callback, which v0.1 refuses on async paths.

OcxProcessError

The child exited non-zero under check.

OcxTimeoutError

The timeout expired.

Source code in src/ocx_sdk/_client.py
async def exec_async(
    self,
    refs: Sequence[PackageLike],
    argv: Sequence[str],
    *,
    platform: str | None = None,
    clean: bool = False,
    private: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    capture: bool = True,
    check: bool = True,
    timeout: MaybeTimeout = UNSET,
) -> CommandResult:
    """Run a command in a packages' environment, on the event loop.

    Args:
        refs: Package identifiers to compose.
        argv: The command and its arguments. Must not be empty.
        platform: The platform to resolve against.
        clean: Strip the ambient parent environment before composing.
        private: Compose the private surface — ocx's `--self`.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        records_dir: Write an execution record — the resolved closure
            and executable — under this directory. `None` leaves it to
            `[records] dir` and `OCX_RECORDS_DIR`; with none of the
            three, no record is written.
        records_name: The record's filename template, over `{time}`,
            `{host}`, `{pid}` and `{rand}`. `None` leaves it to
            `[records] name` and `OCX_RECORDS_NAME`.
        capture: Pipe and capture both streams.
        check: Raise on a non-zero child exit instead of returning it.
        timeout: Seconds for the whole run. Omitted takes the config's.

    Returns:
        The child's exit code and, under `capture`, its output.

    Raises:
        ValueError: `argv` is empty, or the handle carries an `on_log`
            callback, which v0.1 refuses on async paths.
        OcxProcessError: The child exited non-zero under `check`.
        OcxTimeoutError: The timeout expired.
    """
    composed = self._exec_argv(refs, argv, platform, clean, private, env, lazy_mode, records_dir, records_name)
    await self._runner.gate_async()
    return await self._runner.finish_async(composed, capture=capture, check=check, timeout=timeout, retry=None)

spawn

spawn(refs: Sequence[PackageLike], argv: Sequence[str], *, platform: str | None = None, clean: bool = False, private: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, records_dir: str | Path | None = None, records_name: str | None = None, **popen_kw: Any) -> Popen[Any]

Start a command in a packages' environment and return Popen.

PARAMETER DESCRIPTION
refs

Package identifiers to compose.

TYPE: Sequence[PackageLike]

argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

private

Compose the private surface — ocx's --self.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

records_dir

Write an execution record — the resolved closure and executable — under this directory. None leaves it to [records] dir and OCX_RECORDS_DIR; with none of the three, no record is written.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template, over {time}, {host}, {pid} and {rand}. None leaves it to [records] name and OCX_RECORDS_NAME.

TYPE: str | None DEFAULT: None

**popen_kw

Forwarded to Popen. args, shell, and executable are rejected.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Popen[Any]

The running child.

RAISES DESCRIPTION
ValueError

argv is empty, or a rejected keyword was passed.

Source code in src/ocx_sdk/_client.py
def spawn(
    self,
    refs: Sequence[PackageLike],
    argv: Sequence[str],
    *,
    platform: str | None = None,
    clean: bool = False,
    private: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    **popen_kw: Any,
) -> subprocess.Popen[Any]:
    """Start a command in a packages' environment and return `Popen`.

    Args:
        refs: Package identifiers to compose.
        argv: The command and its arguments. Must not be empty.
        platform: The platform to resolve against.
        clean: Strip the ambient parent environment before composing.
        private: Compose the private surface — ocx's `--self`.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        records_dir: Write an execution record — the resolved closure
            and executable — under this directory. `None` leaves it to
            `[records] dir` and `OCX_RECORDS_DIR`; with none of the
            three, no record is written.
        records_name: The record's filename template, over `{time}`,
            `{host}`, `{pid}` and `{rand}`. `None` leaves it to
            `[records] name` and `OCX_RECORDS_NAME`.
        **popen_kw: Forwarded to `Popen`. `args`, `shell`, and
            `executable` are rejected.

    Returns:
        The running child.

    Raises:
        ValueError: `argv` is empty, or a rejected keyword was passed.
    """
    composed = self._exec_argv(refs, argv, platform, clean, private, env, lazy_mode, records_dir, records_name)
    self._runner.gate()
    return self._runner.launch(composed, popen_kw)

spawn_async async

spawn_async(refs: Sequence[PackageLike], argv: Sequence[str], *, platform: str | None = None, clean: bool = False, private: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, records_dir: str | Path | None = None, records_name: str | None = None, **popen_kw: Any) -> Process

Start a command in a packages' environment, on the event loop.

PARAMETER DESCRIPTION
refs

Package identifiers to compose.

TYPE: Sequence[PackageLike]

argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

private

Compose the private surface — ocx's --self.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

records_dir

Write an execution record — the resolved closure and executable — under this directory. None leaves it to [records] dir and OCX_RECORDS_DIR; with none of the three, no record is written.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template, over {time}, {host}, {pid} and {rand}. None leaves it to [records] name and OCX_RECORDS_NAME.

TYPE: str | None DEFAULT: None

**popen_kw

Forwarded to the subprocess factory. args, shell, and executable are rejected.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Process

The running child.

RAISES DESCRIPTION
ValueError

argv is empty, or a rejected keyword was passed.

Source code in src/ocx_sdk/_client.py
async def spawn_async(
    self,
    refs: Sequence[PackageLike],
    argv: Sequence[str],
    *,
    platform: str | None = None,
    clean: bool = False,
    private: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    **popen_kw: Any,
) -> asyncio.subprocess.Process:
    """Start a command in a packages' environment, on the event loop.

    Args:
        refs: Package identifiers to compose.
        argv: The command and its arguments. Must not be empty.
        platform: The platform to resolve against.
        clean: Strip the ambient parent environment before composing.
        private: Compose the private surface — ocx's `--self`.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        records_dir: Write an execution record — the resolved closure
            and executable — under this directory. `None` leaves it to
            `[records] dir` and `OCX_RECORDS_DIR`; with none of the
            three, no record is written.
        records_name: The record's filename template, over `{time}`,
            `{host}`, `{pid}` and `{rand}`. `None` leaves it to
            `[records] name` and `OCX_RECORDS_NAME`.
        **popen_kw: Forwarded to the subprocess factory. `args`,
            `shell`, and `executable` are rejected.

    Returns:
        The running child.

    Raises:
        ValueError: `argv` is empty, or a rejected keyword was passed.
    """
    composed = self._exec_argv(refs, argv, platform, clean, private, env, lazy_mode, records_dir, records_name)
    await self._runner.gate_async()
    return await self._runner.launch_async(composed, popen_kw)

which

which(*refs: PackageLike, platform: str | None = None, resolve: Resolve | None = None, lazy_mode: LazyMode | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> Mapping[str, WhichResult]

Resolve installed packages to their paths, downloading nothing.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

resolve

Which store symlink to resolve through — 'candidate' or 'current'. None leaves the choice to ocx.

TYPE: Resolve | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
Mapping[str, WhichResult]

A path and kind per identifier. This payload is doc-flagged

Mapping[str, WhichResult]

pre-1.0 upstream and may change shape.

Source code in src/ocx_sdk/_client.py
def which(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    resolve: Resolve | None = None,
    lazy_mode: LazyMode | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> Mapping[str, WhichResult]:
    """Resolve installed packages to their paths, downloading nothing.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        resolve: Which store symlink to resolve through — `'candidate'`
            or `'current'`. `None` leaves the choice to ocx.
        lazy_mode: When content downloads — now, or on first use.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        A path and kind per identifier. This payload is doc-flagged
        pre-1.0 upstream and may change shape.
    """
    command = [
        "package",
        "which",
        *_resolve_flag(resolve),
        *_flag("--platform", platform),
        *_flag("--lazy-mode", lazy_mode),
    ]
    return parse_which(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

inspect

inspect(*refs: PackageLike, platform: str | None = None, env: Mapping[str, EnvValue] | None = None, resolve: bool = False, closure: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> InspectReport

Inspect packages straight from the registry or the store.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

resolve

Add the pinned identifier, digest, layers, and the resolution chain, which at this tier starts with an index hop the project tier skips.

TYPE: bool DEFAULT: False

closure

Add the dependency closure, its surface, and any conflicts.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
InspectReport

The inspected packages, whose name is the full requested

InspectReport

identifier at this tier.

Source code in src/ocx_sdk/_client.py
def inspect(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    env: Mapping[str, EnvValue] | None = None,
    resolve: bool = False,
    closure: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> InspectReport:
    """Inspect packages straight from the registry or the store.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        env: Extra `[env]` entries for this call.
        resolve: Add the pinned identifier, digest, layers, and the
            resolution chain, which at this tier starts with an index
            hop the project tier skips.
        closure: Add the dependency closure, its surface, and any
            conflicts.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The inspected packages, whose `name` is the full requested
        identifier at this tier.
    """
    command = [
        "package",
        "inspect",
        *_flag("--platform", platform),
        *_switch("--resolve", resolve),
        *_switch("--closure", closure),
        *_env_flags(env),
    ]
    return InspectReport.from_json(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

description_pull

description_pull(*refs: PackageLike, save_readme: str | Path | None = None, save_logo: str | Path | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> InfoResult

Show the description metadata a registry holds for packages.

PARAMETER DESCRIPTION
*refs

Package repositories to query.

TYPE: PackageLike DEFAULT: ()

save_readme

Write the README to this file or directory. ocx accepts it for a single package only.

TYPE: str | Path | None DEFAULT: None

save_logo

Write the logo to this file or directory. Single package only, as above.

TYPE: str | Path | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
InfoResult

One entry per identifier as given, None where the registry

InfoResult

holds no description metadata.

RAISES DESCRIPTION
ValueError

A save target was given for anything other than exactly one package — ocx would refuse it, and refusing here names which argument to drop.

Source code in src/ocx_sdk/_client.py
def description_pull(
    self,
    *refs: PackageLike,
    save_readme: str | Path | None = None,
    save_logo: str | Path | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> InfoResult:
    """Show the description metadata a registry holds for packages.

    Args:
        *refs: Package repositories to query.
        save_readme: Write the README to this file or directory. ocx
            accepts it for a single package only.
        save_logo: Write the logo to this file or directory. Single
            package only, as above.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        One entry per identifier as given, `None` where the registry
        holds no description metadata.

    Raises:
        ValueError: A save target was given for anything other than
            exactly one package — ocx would refuse it, and refusing here
            names which argument to drop.
    """
    if (save_readme is not None or save_logo is not None) and len(refs) != 1:
        raise ValueError(
            f"save_readme and save_logo write one file, so ocx takes them for a single package only; "
            f"{len(refs)} were given. Call description_pull() once per package, or drop the save target."
        )
    command = [
        "package",
        "description",
        "pull",
        *_flag("--save-readme", save_readme),
        *_flag("--save-logo", save_logo),
    ]
    return parse_description_pull(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

deps

deps(*refs: PackageLike, platform: str | None = None, private: bool = False, why: PackageLike | None = None, depth: int | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> DepsReport

Show the dependency tree of installed packages.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

private

Include the private, self-only edges — ocx's --self. Generated launchers pass it; a consumer needs it only when building a launcher equivalent.

TYPE: bool DEFAULT: False

why

Explain why this dependency is pulled in. Matched by registry and repository; the tag is ignored.

TYPE: PackageLike | None DEFAULT: None

depth

Limit the tree depth. None is unlimited.

TYPE: int | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
DepsReport

One root per requested package.

Source code in src/ocx_sdk/_client.py
def deps(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    private: bool = False,
    why: PackageLike | None = None,
    depth: int | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> DepsReport:
    """Show the dependency tree of installed packages.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        private: Include the private, self-only edges — ocx's `--self`.
            Generated launchers pass it; a consumer needs it only when
            building a launcher equivalent.
        why: Explain why this dependency is pulled in. Matched by
            registry and repository; the tag is ignored.
        depth: Limit the tree depth. `None` is unlimited.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        One root per requested package.
    """
    # ponytail: --flat is not wrapped — it replaces the tree with a flat
    # evaluation order, so it needs a result-type decision `DepsReport`
    # cannot absorb. invoke() reaches it meanwhile.
    command = [
        "package",
        "deps",
        *_switch("--self", private),
        *_flag("--why", why),
        *_flag("--depth", depth),
        *_flag("--platform", platform),
    ]
    return DepsReport.from_json(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

pull

pull(*refs: PackageLike, platform: str | None = None, verify: bool | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> Mapping[str, str]

Download packages into the store without creating symlinks.

PARAMETER DESCRIPTION
*refs

Package identifiers.

TYPE: PackageLike DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

verify

Verify each package's Sigstore signature before storing it. None leaves ocx's default, which is on. The gate fires only where a [[trust.policy]] covers the package, so True against an uncovered package is a documented no-op rather than enforcement. False also beats an ambient OCX_NO_VERIFY, which the SDK neutralizes on every spawn.

TYPE: bool | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
Mapping[str, str]

A store path per identifier — a bare string here, unlike the

Mapping[str, str]

project tier's pull and unlike which.

RAISES DESCRIPTION
OcxProcessError

ocx 0.6 verifies a covered package's signature before storing it, so this call can fail where the identical call against a 0.5.x binary succeeded — no code change on the caller's side, only a newer binary. The exit codes, and why none of them retry, are in the guide's "Errors & credentials" section on verification.

Source code in src/ocx_sdk/_client.py
def pull(
    self,
    *refs: PackageLike,
    platform: str | None = None,
    verify: bool | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> Mapping[str, str]:
    """Download packages into the store without creating symlinks.

    Args:
        *refs: Package identifiers.
        platform: The platform to resolve against.
        verify: Verify each package's Sigstore signature before storing
            it. `None` leaves ocx's default, which is on. The gate fires
            only where a `[[trust.policy]]` covers the package, so `True`
            against an uncovered package is a documented no-op rather
            than enforcement. `False` also beats an ambient
            `OCX_NO_VERIFY`, which the SDK neutralizes on every spawn.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        A store path per identifier — a bare string here, unlike the
        project tier's `pull` and unlike `which`.

    Raises:
        OcxProcessError: ocx 0.6 verifies a covered package's signature
            before storing it, so this call can fail where the identical
            call against a 0.5.x binary succeeded — no code change on the
            caller's side, only a newer binary. The exit codes, and why
            none of them retry, are in the guide's "Errors & credentials"
            section on verification.
    """
    command = ["package", "pull", *_flag("--platform", platform), *_toggle("--verify", "--no-verify", verify)]
    return parse_package_pull(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

create

create(path: str | Path, *, identifier: str | None = None, platform: str | None = None, output: str | Path | None = None, metadata: str | Path | None = None, force: bool = False, compression_level: Literal['fast', 'best', 'default'] | None = None, threads: int | None = None, bin_scan: bool | None = None, libc_lint: bool = True, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> None

Bundle a local directory into a package archive.

Writes the bundle, a compiled metadata sidecar when metadata is given, and a build receipt that push and test read back. There is nothing to return: ocx prints no payload for this command.

PARAMETER DESCRIPTION
path

The package directory to bundle.

TYPE: str | Path

identifier

The identifier the bundle will be published under. Recorded in the receipt, where push finds it.

TYPE: str | None DEFAULT: None

platform

The platform the content runs on. Required whenever metadata is given.

TYPE: str | None DEFAULT: None

output

The output file or directory.

TYPE: str | Path | None DEFAULT: None

metadata

A metadata.json to validate, resolve, and write beside the bundle.

TYPE: str | Path | None DEFAULT: None

force

Overwrite an existing output file.

TYPE: bool DEFAULT: False

compression_level

How hard to compress the bundle. None leaves ocx's own default.

TYPE: Literal['fast', 'best', 'default'] | None DEFAULT: None

threads

Compression threads. 0 auto-detects, 1 is single-threaded; None leaves ocx's own default.

TYPE: int | None DEFAULT: None

bin_scan

Scan the content tree for the executables the package puts on PATH, verifying a declared binaries claim or filling an absent one. Needs metadata. None leaves ocx's own default, which fills an absent claim.

TYPE: bool | None DEFAULT: None

libc_lint

Check the packaged binaries against the platform's declared os.features. False is an escape hatch, not a convenience: it publishes a package that may not run on hosts the declaration claims.

TYPE: bool DEFAULT: True

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

Source code in src/ocx_sdk/_client.py
def create(
    self,
    path: str | Path,
    *,
    identifier: str | None = None,
    platform: str | None = None,
    output: str | Path | None = None,
    metadata: str | Path | None = None,
    force: bool = False,
    compression_level: Literal["fast", "best", "default"] | None = None,
    threads: int | None = None,
    bin_scan: bool | None = None,
    libc_lint: bool = True,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> None:
    """Bundle a local directory into a package archive.

    Writes the bundle, a compiled metadata sidecar when `metadata` is
    given, and a build receipt that `push` and `test` read back. There is
    nothing to return: ocx prints no payload for this command.

    Args:
        path: The package directory to bundle.
        identifier: The identifier the bundle will be published under.
            Recorded in the receipt, where `push` finds it.
        platform: The platform the content runs on. Required whenever
            `metadata` is given.
        output: The output file or directory.
        metadata: A `metadata.json` to validate, resolve, and write
            beside the bundle.
        force: Overwrite an existing output file.
        compression_level: How hard to compress the bundle. `None`
            leaves ocx's own default.
        threads: Compression threads. `0` auto-detects, `1` is
            single-threaded; `None` leaves ocx's own default.
        bin_scan: Scan the content tree for the executables the package
            puts on `PATH`, verifying a declared `binaries` claim or
            filling an absent one. Needs `metadata`. `None` leaves ocx's
            own default, which fills an absent claim.
        libc_lint: Check the packaged binaries against the platform's
            declared `os.features`. `False` is an escape hatch, not a
            convenience: it publishes a package that may not run on
            hosts the declaration claims.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.
    """
    command = [
        "package",
        "create",
        *_flag("--identifier", identifier),
        *_flag("--platform", platform),
        *_flag("--output", output),
        *_flag("--metadata", metadata),
        *_switch("--force", force),
        *_flag("--compression-level", compression_level),
        *_flag("--threads", threads),
        *_toggle("--bin-scan", "--no-bin-scan", bin_scan),
        *_switch("--no-libc-lint", not libc_lint),
    ]
    self._call(command, (str(path),), timeout=timeout, retry=retry)

test

test(identifier: str, *, script: str | Path, layers: Iterable[str | Path] = (), platform: str | None = None, metadata: str | Path | None = None, output: str | Path | None = None, keep: bool = False, clean: bool = False, private: bool = False, env: Mapping[str, EnvValue] | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> TestResult

Materialize a package locally and run a Starlark test script.

Only the --script form is typed. ocx's trailing -- CMD form prints the child's stdout verbatim even under --format json, so it produces nothing parseable — reach for invoke if you need it.

PARAMETER DESCRIPTION
identifier

The identifier to materialize under. Tag form only.

TYPE: str

script

Path to the Starlark test script, or - to read the script source from stdin.

TYPE: str | Path

layers

Layer archives or digest references, base first.

TYPE: Iterable[str | Path] DEFAULT: ()

platform

The platform to resolve against. Omitted reads the build receipt beside the bundle.

TYPE: str | None DEFAULT: None

metadata

The compiled metadata sidecar. Required when no file layers are given.

TYPE: str | Path | None DEFAULT: None

output

Where to materialize the package. None uses ocx's temp root under $OCX_HOME.

TYPE: str | Path | None DEFAULT: None

keep

Preserve the temporary build directory.

TYPE: bool DEFAULT: False

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

private

Compose the private surface — ocx's --self.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
TestResult

The outcome: status decides, and assertion.kind is the

TestResult

stable machine-readable reason. A failing script is a RESULT

TestResult

(exit 1 still carries the envelope), not an exception — only

TestResult

resolution or usage failures raise.

Source code in src/ocx_sdk/_client.py
def test(
    self,
    identifier: str,
    *,
    script: str | Path,
    layers: Iterable[str | Path] = (),
    platform: str | None = None,
    metadata: str | Path | None = None,
    output: str | Path | None = None,
    keep: bool = False,
    clean: bool = False,
    private: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> TestResult:
    """Materialize a package locally and run a Starlark test script.

    Only the `--script` form is typed. ocx's trailing `-- CMD` form
    prints the child's stdout verbatim even under `--format json`, so it
    produces nothing parseable — reach for `invoke` if you need it.

    Args:
        identifier: The identifier to materialize under. Tag form only.
        script: Path to the Starlark test script, or `-` to read the
            script source from stdin.
        layers: Layer archives or digest references, base first.
        platform: The platform to resolve against. Omitted reads the
            build receipt beside the bundle.
        metadata: The compiled metadata sidecar. Required when no file
            layers are given.
        output: Where to materialize the package. `None` uses ocx's
            temp root under `$OCX_HOME`.
        keep: Preserve the temporary build directory.
        clean: Strip the ambient parent environment before composing.
        private: Compose the private surface — ocx's `--self`.
        env: Extra `[env]` entries for this call.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The outcome: `status` decides, and `assertion.kind` is the
        stable machine-readable reason. A failing script is a RESULT
        (exit 1 still carries the envelope), not an exception — only
        resolution or usage failures raise.
    """
    command = [
        "package",
        "test",
        "--identifier",
        identifier,
        "--script",
        str(script),
        *_flag("--platform", platform),
        *_flag("--metadata", metadata),
        *_flag("--output", output),
        *_switch("--keep", keep),
        *_switch("--clean", clean),
        *_switch("--self", private),
        *_env_flags(env),
    ]
    positionals = tuple(str(layer) for layer in layers)
    # Exit 1 is a failed assertion WITH the v1 envelope on stdout — a
    # result to parse, not a process failure (WP13 finding).
    result = self._call(command, positionals, timeout=timeout, retry=retry, ok_codes=(0, 1))
    return TestResult.from_json(result.stdout)

push

push(*layers: str | Path, identifier: str | None = None, platform: str | None = None, metadata: str | Path | None = None, cascade: bool = False, keep_tag: bool | None = None, build_timestamp: Literal['datetime', 'date', 'none'] | None = None, annotations: Mapping[str, str] | None = None, tags_file: str | Path | None = None, sign: bool = False, key: str | None = None, signature_format: SignatureFormat | None = None, rekor_upload: bool | None = None, sbom: str | Path | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> PushResult

Publish a package's layers and metadata to a registry.

Retries are off by default: a push is a registry write, and re-sending one on a timeout can publish twice. Pass retry= explicitly when the target is known to be idempotent-safe.

A push that lands and then fails to sign or attest is not rolled back — a pushed manifest is immutable and OCI offers no un-push — and the signing failure decides the exit code, so the call raises. The report survives that: push writes its payload bare at the JSON root (D11), so catch OcxProcessError, recover the document with partial_report(err), and parse it with PushResult.from_json. The package is published, and signatures names which platform failed.

PARAMETER DESCRIPTION
*layers

Layer archives or sha256:<hex>.<ext> references, base first, each optionally carrying a :strip=,prefix=,from= tail.

TYPE: str | Path DEFAULT: ()

identifier

The identifier to publish under. Omitted reads the build receipt beside the bundle.

TYPE: str | None DEFAULT: None

platform

The platform the manifest is scoped to. Omitted reads the receipt.

TYPE: str | None DEFAULT: None

metadata

The compiled metadata sidecar. Required when no file layers are given.

TYPE: str | Path | None DEFAULT: None

cascade

Also advance the rolling tags above this version.

TYPE: bool DEFAULT: False

keep_tag

Write the __ocx.keep.sha256-<hex> tag for each platform manifest published. None leaves ocx's default, which writes it.

TYPE: bool | None DEFAULT: None

build_timestamp

Append a UTC build-metadata segment to the published tag, for rolling continuous-deploy versions. The version core in identifier must already be X.Y.Z.

TYPE: Literal['datetime', 'date', 'none'] | None DEFAULT: None

annotations

OCI annotations for the published index.

TYPE: Mapping[str, str] | None DEFAULT: None

tags_file

Append the pushed tag and any cascade tags to this file, where ocx package announce --tags-file picks them up. A scratch file for one pipeline run, not a persistent list.

TYPE: str | Path | None DEFAULT: None

sign

Sign each platform manifest this push writes, inline. Required — along with sbom — before any of signature_format, key and rekor_upload mean anything: ocx refuses a signing modifier with nothing to sign. Off by default — a push without it signs nothing. The signature covers each platform manifest, never the image index, whose digest is rewritten every time another platform merges into it; sign the index afterwards with sign(tags_file=...), reading the file tags_file wrote.

TYPE: bool DEFAULT: False

key

A key reference for the inline signing — file:// or env://, the two backends ocx 0.6 implements, or a bare path, read as file://. The five KMS schemes parse and are then refused with exit 85 (UnsupportedKeyBackendError). None signs keyless, against Fulcio.

TYPE: str | None DEFAULT: None

signature_format

Which signature format(s) the inline signing produces.

TYPE: SignatureFormat | None DEFAULT: None

rekor_upload

Upload the inline signature to the transparency log. False (--no-rekor-upload) is valid only alongside key: a keyless signature must be logged, since its Fulcio certificate lives about ten minutes and the log entry's timestamp is the only lasting proof it was signed while the certificate was valid.

TYPE: bool | None DEFAULT: None

sbom

After the push, attest this CycloneDX SBOM against the pushed manifest — sugar for attest(predicate_type= "cyclonedx") on the digest this push just wrote. Read before the push, so a bad path costs no upload.

TYPE: str | Path | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
PushResult

The published identifier, digest, and tags.

RAISES DESCRIPTION
ValueError

A signing modifier (signature_format, key, rekor_upload) was given without sign=True or sbom=; or rekor_upload=False was given without key.

OcxProcessError

A non-zero exit. When the push itself landed and the signing or the SBOM attestation is what failed, see the partial-failure note above for how to recover the report.

Source code in src/ocx_sdk/_client.py
def push(
    self,
    *layers: str | Path,
    identifier: str | None = None,
    platform: str | None = None,
    metadata: str | Path | None = None,
    cascade: bool = False,
    keep_tag: bool | None = None,
    build_timestamp: Literal["datetime", "date", "none"] | None = None,
    annotations: Mapping[str, str] | None = None,
    tags_file: str | Path | None = None,
    sign: bool = False,
    key: str | None = None,
    signature_format: SignatureFormat | None = None,
    rekor_upload: bool | None = None,
    sbom: str | Path | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> PushResult:
    """Publish a package's layers and metadata to a registry.

    Retries are off by default: a push is a registry write, and
    re-sending one on a timeout can publish twice. Pass `retry=`
    explicitly when the target is known to be idempotent-safe.

    A push that lands and then fails to sign or attest is **not rolled
    back** — a pushed manifest is immutable and OCI offers no un-push —
    and the signing failure decides the exit code, so the call raises.
    The report survives that: `push` writes its payload bare at the JSON
    root (D11), so catch `OcxProcessError`, recover the document with
    `partial_report(err)`, and parse it with `PushResult.from_json`. The
    package is published, and `signatures` names which platform failed.

    Args:
        *layers: Layer archives or `sha256:<hex>.<ext>` references, base
            first, each optionally carrying a `:strip=,prefix=,from=`
            tail.
        identifier: The identifier to publish under. Omitted reads the
            build receipt beside the bundle.
        platform: The platform the manifest is scoped to. Omitted reads
            the receipt.
        metadata: The compiled metadata sidecar. Required when no file
            layers are given.
        cascade: Also advance the rolling tags above this version.
        keep_tag: Write the `__ocx.keep.sha256-<hex>` tag for each platform
            manifest published. `None` leaves ocx's default, which writes it.
        build_timestamp: Append a UTC build-metadata segment to the
            published tag, for rolling continuous-deploy versions. The
            version core in `identifier` must already be `X.Y.Z`.
        annotations: OCI annotations for the published index.
        tags_file: Append the pushed tag and any cascade tags to this
            file, where `ocx package announce --tags-file` picks
            them up. A scratch file for one pipeline run, not a
            persistent list.
        sign: Sign each platform manifest this push writes, inline.
            Required — along with `sbom` — before any of
            `signature_format`, `key` and `rekor_upload` mean anything:
            ocx refuses a signing modifier with nothing to sign.
            Off by default — a push without it signs nothing. The
            signature covers each platform manifest, never the image
            index, whose digest is rewritten every time another platform
            merges into it; sign the index afterwards with
            `sign(tags_file=...)`, reading the file `tags_file` wrote.
        key: A key reference for the inline signing — `file://` or
            `env://`, the two backends ocx 0.6 implements, or a bare
            path, read as `file://`. The five KMS schemes parse and are
            then refused with exit 85 (`UnsupportedKeyBackendError`).
            `None` signs keyless, against Fulcio.
        signature_format: Which signature format(s) the inline signing
            produces.
        rekor_upload: Upload the inline signature to the transparency
            log. `False` (`--no-rekor-upload`) is valid only alongside
            `key`: a keyless signature must be logged, since its Fulcio
            certificate lives about ten minutes and the log entry's
            timestamp is the only lasting proof it was signed while the
            certificate was valid.
        sbom: After the push, attest this CycloneDX SBOM against the
            pushed manifest — sugar for `attest(predicate_type=
            "cyclonedx")` on the digest this push just wrote. Read
            before the push, so a bad path costs no upload.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries.

    Returns:
        The published identifier, digest, and tags.

    Raises:
        ValueError: A signing modifier (`signature_format`, `key`,
            `rekor_upload`) was given without `sign=True` or `sbom=`; or
            `rekor_upload=False` was given without `key`.
        OcxProcessError: A non-zero exit. When the push itself landed and
            the signing or the SBOM attestation is what failed, see the
            partial-failure note above for how to recover the report.
    """
    modifiers = sorted(
        name
        for name, given in (
            ("signature_format", signature_format is not None),
            ("key", key is not None),
            ("rekor_upload", rekor_upload is not None),
        )
        if given
    )
    if modifiers and not (sign or sbom is not None):
        raise ValueError(
            f"{', '.join(modifiers)} modifies signing and this push signs nothing, so ocx refuses it "
            f"(exit 64) rather than accepting an argument it would never read. Pass sign=True, or sbom= "
            f"to attest one, or drop the signing arguments."
        )
    _no_rekor_upload_needs_key(key, rekor_upload)
    command = [
        "package",
        "push",
        *_flag("--identifier", identifier),
        *_switch("--cascade", cascade),
        *_toggle("--keep-tag", "--no-keep-tag", keep_tag),
        # The equals form is mandatory: bare `--build-timestamp` means
        # `datetime`, and a space-separated value is a usage error.
        *([] if build_timestamp is None else [f"--build-timestamp={build_timestamp}"]),
        *_flag("--metadata", metadata),
        *_repeated("--annotation", (f"{name}={value}" for name, value in (annotations or {}).items())),
        *_flag("--tags-file", tags_file),
        *_flag("--platform", platform),
        *_switch("--sign", sign),
        *_flag("--signature-format", signature_format),
        *_flag("--key", key),
        *_toggle("--rekor-upload", "--no-rekor-upload", rekor_upload),
        *_flag("--sbom", sbom),
    ]
    positionals = tuple(str(layer) for layer in layers)
    result = self._call(command, positionals, timeout=timeout, retry=retry, mutating=True)
    return PushResult.from_json(result.stdout)

description_push

description_push(identifier: PackageLike, *, from_: PackageLike | None = None, readme: str | Path | None = None, logo: str | Path | None = None, title: str | None = None, description: str | None = None, keywords: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CommandResult

Publish description metadata for a package repository (C-003).

ocx prints no JSON payload for this command (D9) — it logs and exits, so there is nothing typed to return.

PARAMETER DESCRIPTION
identifier

The package repository to describe.

TYPE: PackageLike

from_

Another package to copy the description from, instead of the field arguments below — ocx's --from takes a package identifier, not a path (package_description_push.rs:26-27). Mutually exclusive with readme, logo, title, description, and keywords.

TYPE: PackageLike | None DEFAULT: None

readme

The README file to publish.

TYPE: str | Path | None DEFAULT: None

logo

The logo file to publish.

TYPE: str | Path | None DEFAULT: None

title

The display title to publish.

TYPE: str | None DEFAULT: None

description

The prose description to publish.

TYPE: str | None DEFAULT: None

keywords

A single delimited keywords string, not a list — ocx's own field is a bare string (C-021).

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries — this is a registry write (D5).

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The exit code and whatever ocx printed.

RAISES DESCRIPTION
ValueError

Neither from_ nor any of the five field arguments was given, or from_ was given alongside one of them.

NotFoundError

from_ names a source that carries no description (exit 79).

Source code in src/ocx_sdk/_client.py
def description_push(
    self,
    identifier: PackageLike,
    *,
    from_: PackageLike | None = None,
    readme: str | Path | None = None,
    logo: str | Path | None = None,
    title: str | None = None,
    description: str | None = None,
    keywords: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CommandResult:
    """Publish description metadata for a package repository (C-003).

    ocx prints no JSON payload for this command (D9) — it logs and
    exits, so there is nothing typed to return.

    Args:
        identifier: The package repository to describe.
        from_: Another package to copy the description from, instead of
            the field arguments below — ocx's `--from` takes a package
            identifier, not a path (`package_description_push.rs:26-27`).
            Mutually exclusive with `readme`, `logo`, `title`,
            `description`, and `keywords`.
        readme: The README file to publish.
        logo: The logo file to publish.
        title: The display title to publish.
        description: The prose description to publish.
        keywords: A single delimited keywords string, **not** a list —
            ocx's own field is a bare string (C-021).
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries — this is a
            registry write (D5).

    Returns:
        The exit code and whatever ocx printed.

    Raises:
        ValueError: Neither `from_` nor any of the five field arguments
            was given, or `from_` was given alongside one of them.
        NotFoundError: `from_` names a source that carries no
            description (exit 79).
    """
    fields = {"readme": readme, "logo": logo, "title": title, "description": description, "keywords": keywords}
    given = sorted(name for name, value in fields.items() if value is not None)
    if from_ is None and not given:
        raise ValueError(
            "there is nothing to publish: ocx needs from_ to copy a description, or at least one of "
            "readme, logo, title, description and keywords to author one."
        )
    if from_ is not None and given:
        raise ValueError(
            f"from_ copies a whole description and the field arguments author one, so ocx refuses them "
            f"together rather than silently picking a winner; {', '.join(given)} came with from_. "
            f"Drop from_ to publish these fields, or drop them to promote the description as it stands."
        )
    command = [
        "package",
        "description",
        "push",
        *_flag("--from", from_),
        *_flag("--readme", readme),
        *_flag("--logo", logo),
        *_flag("--title", title),
        *_flag("--description", description),
        *_flag("--keywords", keywords),
    ]
    return self._call(command, (str(identifier),), timeout=timeout, retry=retry, mutating=True)

sign

sign(ref: PackageLike, *, tags: Iterable[str], tags_file: str | Path | None = None, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SweepReport
sign(ref: PackageLike, *, tags: Iterable[str] | None = None, tags_file: str | Path, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SweepReport
sign(ref: PackageLike, *, tags: None = None, tags_file: None = None, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SignatureReport
sign(ref: PackageLike, *, tags: Iterable[str] | None = None, tags_file: str | Path | None = None, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SignatureReport | SweepReport

Sign a package reference with cosign/Sigstore (C-011).

Three call shapes, one method (D2): tags or tags_file sweeps every matching tag and returns a SweepReport; neither signs ref alone and returns a SignatureReport.

mutating=True (D5) — retries are off by default; auto-retrying a Rekor-unavailable failure would amplify the public instance's rate limiting (D6).

A --signature-format both run where one leg lands and one fails exits non-zero carrying a full report: catch OcxProcessError, recover it with partial_report(err), and parse with SignatureReport.from_json (D10) — SignatureLegReport.error names which leg died.

key and keyless signing are mutually exclusive with each other's machinery, not just in spirit. Four flags refuse key outright — fulcio_url, identity_token_file, identity_token_stdin, and no_tty. Separately, rekor_upload=False (--no-rekor-upload) requires key — the opposite direction: it is meaningless for keyless signing, which cannot skip Rekor. And independently of key entirely, identity_token_file and identity_token_stdin conflict with each other — two ways to supply one token.

PARAMETER DESCRIPTION
ref

The package reference to sign.

TYPE: PackageLike

tags

Sweep these tags instead of signing ref directly. Mutually exclusive with platform. Unions with tags_file when both are given — sweeping is triggered by either, not a choice between them. An empty sequence is refused: tags=None is how you act on ref itself.

TYPE: Iterable[str] | None DEFAULT: None

tags_file

Sweep the tags listed in this file. Mutually exclusive with platform. Unions with tags — see above.

TYPE: str | Path | None DEFAULT: None

platform

Sign one platform's manifest. Refused alongside tags or tags_file.

TYPE: str | None DEFAULT: None

signature_format

Which signature format(s) to produce.

TYPE: SignatureFormat | None DEFAULT: None

key

A key reference — file:// or env://, the two backends ocx 0.6 implements. A bare path is read as file://. awskms://, gcpkms://, azurekms://, hashivault:// and k8s:// parse and are then refused with exit 85 (UnsupportedKeyBackendError), so no configuration makes them work. None signs keyless, against Fulcio. Conflicts with fulcio_url, identity_token_file, identity_token_stdin, and no_tty.

TYPE: str | None DEFAULT: None

rekor_upload

Upload the signature to the transparency log. False (--no-rekor-upload) is valid only alongside key.

TYPE: bool | None DEFAULT: None

fulcio_url

A non-default Fulcio instance. Conflicts with key.

TYPE: str | None DEFAULT: None

rekor_url

A non-default Rekor instance.

TYPE: str | None DEFAULT: None

identity_token_file

Read the OIDC identity token from this file, for keyless signing without an interactive browser flow. Conflicts with key and with identity_token_stdin.

TYPE: str | Path | None DEFAULT: None

identity_token_stdin

Read the OIDC identity token from stdin. Conflicts with key and with identity_token_file.

TYPE: bool DEFAULT: False

no_tty

Suppress the interactive TTY prompt. Conflicts with key.

TYPE: bool DEFAULT: False

no_cache

Skip ocx's signing cache.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
SignatureReport | SweepReport

A SignatureReport for a single signing, or a SweepReport

SignatureReport | SweepReport

when tags/tags_file swept multiple.

RAISES DESCRIPTION
ValueError

tags was empty; platform was given alongside tags/tags_file; key was given alongside fulcio_url, identity_token_file, identity_token_stdin, or no_tty; identity_token_file and identity_token_stdin were both given; or rekor_upload=False was given without key.

OcxProcessError

A non-zero exit — see the partial-failure note above for how to recover a report from one.

Source code in src/ocx_sdk/_client.py
def sign(
    self,
    ref: PackageLike,
    *,
    tags: Iterable[str] | None = None,
    tags_file: str | Path | None = None,
    platform: str | None = None,
    signature_format: SignatureFormat | None = None,
    key: str | None = None,
    rekor_upload: bool | None = None,
    fulcio_url: str | None = None,
    rekor_url: str | None = None,
    identity_token_file: str | Path | None = None,
    identity_token_stdin: bool = False,
    no_tty: bool = False,
    no_cache: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> SignatureReport | SweepReport:
    """Sign a package reference with cosign/Sigstore (C-011).

    Three call shapes, one method (D2): `tags` or `tags_file` sweeps
    every matching tag and returns a `SweepReport`; neither signs `ref`
    alone and returns a `SignatureReport`.

    `mutating=True` (D5) — retries are off by default; auto-retrying a
    Rekor-unavailable failure would amplify the public instance's rate
    limiting (D6).

    A `--signature-format both` run where one leg lands and one fails
    exits non-zero carrying a full report: catch `OcxProcessError`,
    recover it with `partial_report(err)`, and parse with
    `SignatureReport.from_json` (D10) — `SignatureLegReport.error` names
    which leg died.

    `key` and keyless signing are mutually exclusive with each other's
    machinery, not just in spirit. **Four** flags refuse `key` outright —
    `fulcio_url`, `identity_token_file`, `identity_token_stdin`, and
    `no_tty`. Separately, `rekor_upload=False` (`--no-rekor-upload`)
    *requires* `key` — the opposite direction: it is meaningless for
    keyless signing, which cannot skip Rekor. And independently of
    `key` entirely, `identity_token_file` and `identity_token_stdin`
    conflict with each other — two ways to supply one token.

    Args:
        ref: The package reference to sign.
        tags: Sweep these tags instead of signing `ref` directly.
            Mutually exclusive with `platform`. Unions with `tags_file`
            when both are given — sweeping is triggered by either, not
            a choice between them.
            An empty sequence is refused: `tags=None` is how you act
            on `ref` itself.
        tags_file: Sweep the tags listed in this file. Mutually
            exclusive with `platform`. Unions with `tags` — see above.
        platform: Sign one platform's manifest. Refused alongside
            `tags` or `tags_file`.
        signature_format: Which signature format(s) to produce.
        key: A key reference — `file://` or `env://`, the two backends
            ocx 0.6 implements. A bare path is read as `file://`.
            `awskms://`, `gcpkms://`, `azurekms://`, `hashivault://`
            and `k8s://` parse and are then refused with exit 85
            (`UnsupportedKeyBackendError`), so no configuration makes
            them work. `None` signs keyless, against Fulcio. Conflicts
            with `fulcio_url`, `identity_token_file`,
            `identity_token_stdin`, and `no_tty`.
        rekor_upload: Upload the signature to the transparency log.
            `False` (`--no-rekor-upload`) is valid only alongside `key`.
        fulcio_url: A non-default Fulcio instance. Conflicts with `key`.
        rekor_url: A non-default Rekor instance.
        identity_token_file: Read the OIDC identity token from this
            file, for keyless signing without an interactive browser
            flow. Conflicts with `key` and with
            `identity_token_stdin`.
        identity_token_stdin: Read the OIDC identity token from stdin.
            Conflicts with `key` and with `identity_token_file`.
        no_tty: Suppress the interactive TTY prompt. Conflicts with
            `key`.
        no_cache: Skip ocx's signing cache.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries.

    Returns:
        A `SignatureReport` for a single signing, or a `SweepReport`
        when `tags`/`tags_file` swept multiple.

    Raises:
        ValueError: `tags` was empty; `platform` was given alongside
            `tags`/`tags_file`;
            `key` was given alongside `fulcio_url`,
            `identity_token_file`, `identity_token_stdin`, or `no_tty`;
            `identity_token_file` and `identity_token_stdin` were both
            given; or `rekor_upload=False` was given without `key`.
        OcxProcessError: A non-zero exit — see the partial-failure note
            above for how to recover a report from one.
    """
    sweep_tags, swept = _sweep(tags, tags_file)
    _signing_guards(
        key=key,
        rekor_upload=rekor_upload,
        fulcio_url=fulcio_url,
        identity_token_file=identity_token_file,
        identity_token_stdin=identity_token_stdin,
        no_tty=no_tty,
        platform=platform,
        swept=swept,
    )
    command = [
        "package",
        "sign",
        *_flag("--platform", platform),
        *_flag("--signature-format", signature_format),
        *_flag("--key", key),
        *_toggle("--rekor-upload", "--no-rekor-upload", rekor_upload),
        *_flag("--fulcio-url", fulcio_url),
        *_flag("--rekor-url", rekor_url),
        *_flag("--identity-token-file", identity_token_file),
        *_switch("--identity-token-stdin", identity_token_stdin),
        *_switch("--no-tty", no_tty),
        *_switch("--no-cache", no_cache),
        *_repeated("--tags", sweep_tags),
        *_flag("--tags-file", tags_file),
    ]
    raw = self._call(command, (str(ref),), timeout=timeout, retry=retry, mutating=True).stdout
    if swept:
        return SweepReport.from_json(raw, SignatureReport.from_dict)
    return SignatureReport.from_json(raw)

attest

attest(ref: PackageLike, *, predicate: str | Path, predicate_type: str, tags: Iterable[str], tags_file: str | Path | None = None, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SweepReport
attest(ref: PackageLike, *, predicate: str | Path, predicate_type: str, tags: Iterable[str] | None = None, tags_file: str | Path, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SweepReport
attest(ref: PackageLike, *, predicate: str | Path, predicate_type: str, tags: None = None, tags_file: None = None, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> AttestationReport
attest(ref: PackageLike, *, predicate: str | Path, predicate_type: str, tags: Iterable[str] | None = None, tags_file: str | Path | None = None, platform: str | None = None, signature_format: SignatureFormat | None = None, key: str | None = None, rekor_upload: bool | None = None, fulcio_url: str | None = None, rekor_url: str | None = None, identity_token_file: str | Path | None = None, identity_token_stdin: bool = False, no_tty: bool = False, no_cache: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> AttestationReport | SweepReport

Attach an in-toto attestation to a package reference (C-013).

Same three call shapes as sign (D2): tags/tags_file sweeps and returns a SweepReport; neither attests ref alone and returns an AttestationReport. mutating=True (D5), retries off by default (D6).

Carries the identical --key conflict set sign has — this is enumerated per command rather than delegated to a shared note, because it is attest's own guard, declared independently in package_attest.rs alongside package_sign.rs's: four flags refuse key outright (fulcio_url, identity_token_file, identity_token_stdin, no_tty); rekor_upload=False (--no-rekor-upload) requires key, the opposite direction; and independently of key, identity_token_file conflicts with identity_token_stdin.

PARAMETER DESCRIPTION
ref

The package reference to attest.

TYPE: PackageLike

predicate

The predicate document to attest.

TYPE: str | Path

predicate_type

The predicate type URI or its short alias — ocx's --type. The returned report's predicate_type is the resolved URI, which may differ from what was passed here.

TYPE: str

tags

Sweep these tags instead of attesting ref directly. Unions with tags_file when both are given — sweeping is triggered by either, not a choice between them. An empty sequence is refused: tags=None is how you act on ref itself.

TYPE: Iterable[str] | None DEFAULT: None

tags_file

Sweep the tags listed in this file. Unions with tags — see above.

TYPE: str | Path | None DEFAULT: None

platform

Attest one platform's manifest. Refused alongside tags or tags_file.

TYPE: str | None DEFAULT: None

signature_format

Which signature format(s) to produce.

TYPE: SignatureFormat | None DEFAULT: None

key

A key reference. None signs keyless, against Fulcio. Conflicts with fulcio_url, identity_token_file, identity_token_stdin, and no_tty.

TYPE: str | None DEFAULT: None

rekor_upload

Upload to the transparency log. False (--no-rekor-upload) is valid only alongside key.

TYPE: bool | None DEFAULT: None

fulcio_url

A non-default Fulcio instance. Conflicts with key.

TYPE: str | None DEFAULT: None

rekor_url

A non-default Rekor instance.

TYPE: str | None DEFAULT: None

identity_token_file

Read the OIDC identity token from this file. Conflicts with key and with identity_token_stdin.

TYPE: str | Path | None DEFAULT: None

identity_token_stdin

Read the OIDC identity token from stdin. Conflicts with key and with identity_token_file.

TYPE: bool DEFAULT: False

no_tty

Suppress the interactive TTY prompt. Conflicts with key.

TYPE: bool DEFAULT: False

no_cache

Skip ocx's signing cache.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
AttestationReport | SweepReport

An AttestationReport for a single attestation, or a

AttestationReport | SweepReport

SweepReport when tags/tags_file swept multiple.

RAISES DESCRIPTION
ValueError

tags was empty; platform was given alongside tags/tags_file; key was given alongside fulcio_url, identity_token_file, identity_token_stdin, or no_tty; identity_token_file and identity_token_stdin were both given; or rekor_upload=False was given without key.

OcxProcessError

A non-zero exit. Recover a partial report with partial_report(err) and AttestationReport.from_json (D10).

Source code in src/ocx_sdk/_client.py
def attest(
    self,
    ref: PackageLike,
    *,
    predicate: str | Path,
    predicate_type: str,
    tags: Iterable[str] | None = None,
    tags_file: str | Path | None = None,
    platform: str | None = None,
    signature_format: SignatureFormat | None = None,
    key: str | None = None,
    rekor_upload: bool | None = None,
    fulcio_url: str | None = None,
    rekor_url: str | None = None,
    identity_token_file: str | Path | None = None,
    identity_token_stdin: bool = False,
    no_tty: bool = False,
    no_cache: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> AttestationReport | SweepReport:
    """Attach an in-toto attestation to a package reference (C-013).

    Same three call shapes as `sign` (D2): `tags`/`tags_file` sweeps and
    returns a `SweepReport`; neither attests `ref` alone and returns an
    `AttestationReport`. `mutating=True` (D5), retries off by default
    (D6).

    Carries the identical `--key` conflict set `sign` has — this is
    enumerated per command rather than delegated to a shared note,
    because it is `attest`'s own guard, declared independently in
    `package_attest.rs` alongside `package_sign.rs`'s: **four** flags
    refuse `key` outright (`fulcio_url`, `identity_token_file`,
    `identity_token_stdin`, `no_tty`); `rekor_upload=False`
    (`--no-rekor-upload`) *requires* `key`, the opposite direction; and
    independently of `key`, `identity_token_file` conflicts with
    `identity_token_stdin`.

    Args:
        ref: The package reference to attest.
        predicate: The predicate document to attest.
        predicate_type: The predicate type URI or its short alias —
            ocx's `--type`. The returned report's `predicate_type` is
            the *resolved* URI, which may differ from what was passed
            here.
        tags: Sweep these tags instead of attesting `ref` directly.
            Unions with `tags_file` when both are given — sweeping is
            triggered by either, not a choice between them.
            An empty sequence is refused: `tags=None` is how you act
            on `ref` itself.
        tags_file: Sweep the tags listed in this file. Unions with
            `tags` — see above.
        platform: Attest one platform's manifest. Refused alongside
            `tags` or `tags_file`.
        signature_format: Which signature format(s) to produce.
        key: A key reference. `None` signs keyless, against Fulcio.
            Conflicts with `fulcio_url`, `identity_token_file`,
            `identity_token_stdin`, and `no_tty`.
        rekor_upload: Upload to the transparency log. `False`
            (`--no-rekor-upload`) is valid only alongside `key`.
        fulcio_url: A non-default Fulcio instance. Conflicts with `key`.
        rekor_url: A non-default Rekor instance.
        identity_token_file: Read the OIDC identity token from this
            file. Conflicts with `key` and with `identity_token_stdin`.
        identity_token_stdin: Read the OIDC identity token from stdin.
            Conflicts with `key` and with `identity_token_file`.
        no_tty: Suppress the interactive TTY prompt. Conflicts with
            `key`.
        no_cache: Skip ocx's signing cache.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries.

    Returns:
        An `AttestationReport` for a single attestation, or a
        `SweepReport` when `tags`/`tags_file` swept multiple.

    Raises:
        ValueError: `tags` was empty; `platform` was given alongside
            `tags`/`tags_file`;
            `key` was given alongside `fulcio_url`,
            `identity_token_file`, `identity_token_stdin`, or `no_tty`;
            `identity_token_file` and `identity_token_stdin` were both
            given; or `rekor_upload=False` was given without `key`.
        OcxProcessError: A non-zero exit. Recover a partial report with
            `partial_report(err)` and `AttestationReport.from_json`
            (D10).
    """
    sweep_tags, swept = _sweep(tags, tags_file)
    _signing_guards(
        key=key,
        rekor_upload=rekor_upload,
        fulcio_url=fulcio_url,
        identity_token_file=identity_token_file,
        identity_token_stdin=identity_token_stdin,
        no_tty=no_tty,
        platform=platform,
        swept=swept,
    )
    command = [
        "package",
        "attest",
        "--predicate",
        str(predicate),
        "--type",
        predicate_type,
        *_flag("--platform", platform),
        *_flag("--signature-format", signature_format),
        *_flag("--key", key),
        *_toggle("--rekor-upload", "--no-rekor-upload", rekor_upload),
        *_flag("--fulcio-url", fulcio_url),
        *_flag("--rekor-url", rekor_url),
        *_flag("--identity-token-file", identity_token_file),
        *_switch("--identity-token-stdin", identity_token_stdin),
        *_switch("--no-tty", no_tty),
        *_switch("--no-cache", no_cache),
        *_repeated("--tags", sweep_tags),
        *_flag("--tags-file", tags_file),
    ]
    raw = self._call(command, (str(ref),), timeout=timeout, retry=retry, mutating=True).stdout
    if swept:
        return SweepReport.from_json(raw, AttestationReport.from_dict)
    return AttestationReport.from_json(raw)

verify

verify(ref: PackageLike, *, platform: str | None = None, certificate_identity: str | None = None, certificate_oidc_issuer: str | None = None, key: str | None = None, signature_format: SignatureFormat | None = None, rekor_url: str | None = None, attestation: bool = False, predicate_type: str | None = None, allow_unlogged_signature: bool = False, no_cache: bool = False, sigstore_trusted_root: str | Path | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> VerificationReport

Verify a package reference's signature or attestation (C-012).

A read — not mutating, and keeps the normal retry default (D5).

Keyless verification needs the identity pair: cosign 2.0 made --certificate-identity and --certificate-oidc-issuer hard-required together, because without both a signature from any Fulcio-certified identity passes (D3). Neither is usable alongside key.

PARAMETER DESCRIPTION
ref

The package reference to verify.

TYPE: PackageLike

platform

Verify one platform's manifest.

TYPE: str | None DEFAULT: None

certificate_identity

The pinned keyless identity. Required together with certificate_oidc_issuer for keyless verification.

TYPE: str | None DEFAULT: None

certificate_oidc_issuer

The pinned keyless OIDC issuer. Required together with certificate_identity.

TYPE: str | None DEFAULT: None

key

A key reference, for key-based verification.

TYPE: str | None DEFAULT: None

signature_format

Restrict to one signature format. 'both' is write-side only — it names two shapes, and a result cannot say "either of these satisfied me".

TYPE: SignatureFormat | None DEFAULT: None

rekor_url

A non-default Rekor instance.

TYPE: str | None DEFAULT: None

attestation

Verify an attestation instead of a signature.

TYPE: bool DEFAULT: False

predicate_type

Restrict attestation verification to this predicate type. Requires attestation=True.

TYPE: str | None DEFAULT: None

allow_unlogged_signature

Accept a signature with no transparency log entry.

TYPE: bool DEFAULT: False

no_cache

Skip ocx's verification cache.

TYPE: bool DEFAULT: False

sigstore_trusted_root

A non-default Sigstore trusted root bundle.

TYPE: str | Path | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
VerificationReport

The verified subject, identity, and matching signatures.

RAISES DESCRIPTION
ValueError

Only one of certificate_identity/ certificate_oidc_issuer was given, either was given alongside key, predicate_type was given without attestation=True, or signature_format was 'both'.

OcxProcessError

The signature did not verify — this command's main outcome, not an edge case. DataError (65) for a signature or certificate chain that did not hold up, PermissionDeniedError (77) for an identity or issuer that did not match, NotFoundError (79) when nothing is signed at all, and TransparencyLogUnavailableError (83) when Rekor is unreachable.

Source code in src/ocx_sdk/_client.py
def verify(
    self,
    ref: PackageLike,
    *,
    platform: str | None = None,
    certificate_identity: str | None = None,
    certificate_oidc_issuer: str | None = None,
    key: str | None = None,
    signature_format: SignatureFormat | None = None,
    rekor_url: str | None = None,
    attestation: bool = False,
    predicate_type: str | None = None,
    allow_unlogged_signature: bool = False,
    no_cache: bool = False,
    sigstore_trusted_root: str | Path | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> VerificationReport:
    """Verify a package reference's signature or attestation (C-012).

    A read — not mutating, and keeps the normal retry default (D5).

    Keyless verification needs the identity pair: cosign 2.0 made
    `--certificate-identity` and `--certificate-oidc-issuer`
    hard-required together, because without both a signature from
    *any* Fulcio-certified identity passes (D3). Neither is usable
    alongside `key`.

    Args:
        ref: The package reference to verify.
        platform: Verify one platform's manifest.
        certificate_identity: The pinned keyless identity. Required
            together with `certificate_oidc_issuer` for keyless
            verification.
        certificate_oidc_issuer: The pinned keyless OIDC issuer.
            Required together with `certificate_identity`.
        key: A key reference, for key-based verification.
        signature_format: Restrict to one signature format. `'both'` is
            write-side only — it names two shapes, and a result cannot
            say "either of these satisfied me".
        rekor_url: A non-default Rekor instance.
        attestation: Verify an attestation instead of a signature.
        predicate_type: Restrict attestation verification to this
            predicate type. Requires `attestation=True`.
        allow_unlogged_signature: Accept a signature with no
            transparency log entry.
        no_cache: Skip ocx's verification cache.
        sigstore_trusted_root: A non-default Sigstore trusted root
            bundle.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the
            config's.

    Returns:
        The verified subject, identity, and matching signatures.

    Raises:
        ValueError: Only one of `certificate_identity`/
            `certificate_oidc_issuer` was given, either was given
            alongside `key`, `predicate_type` was given without
            `attestation=True`, or `signature_format` was `'both'`.
        OcxProcessError: The signature did not verify — this command's
            main outcome, not an edge case. `DataError` (65) for a
            signature or certificate chain that did not hold up,
            `PermissionDeniedError` (77) for an identity or issuer that
            did not match, `NotFoundError` (79) when nothing is signed at
            all, and `TransparencyLogUnavailableError` (83) when Rekor is
            unreachable.
    """
    _identity_guards(
        certificate_identity=certificate_identity,
        certificate_oidc_issuer=certificate_oidc_issuer,
        key=key,
    )
    _read_side_format(signature_format)
    if predicate_type is not None and not attestation:
        raise ValueError(
            "predicate_type narrows an attestation, and a signature has no predicate to narrow, so ocx "
            "refuses --type without --attestation. Pass attestation=True, or drop predicate_type."
        )
    command = [
        "package",
        "verify",
        *_flag("--platform", platform),
        *_flag("--certificate-identity", certificate_identity),
        *_flag("--certificate-oidc-issuer", certificate_oidc_issuer),
        *_flag("--key", key),
        *_flag("--signature-format", signature_format),
        *_flag("--rekor-url", rekor_url),
        *_switch("--attestation", attestation),
        *_flag("--type", predicate_type),
        *_switch("--allow-unlogged-signature", allow_unlogged_signature),
        *_switch("--no-cache", no_cache),
        *_flag("--sigstore-trusted-root", sigstore_trusted_root),
    ]
    return VerificationReport.from_json(self._call(command, (str(ref),), timeout=timeout, retry=retry).stdout)

sbom

sbom(ref: PackageLike, *, platform: str | None = None, summary: bool = False, predicate_type: str | None = None, certificate_identity: str | None = None, certificate_oidc_issuer: str | None = None, key: str | None = None, signature_format: SignatureFormat | None = None, sigstore_trusted_root: str | Path | None = None, rekor_url: str | None = None, no_cache: bool = False, verify: bool | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> SbomListingReport

List and verify the SBOM documents attached to a package (C-014).

A read — not mutating. Exits 0 even when candidates were refused; check summary.status, not the exit code, for a "partial_failure" listing.

verify gates the same way install/pull's does (C-008): ocx's default is on, but the gate only fires when a [[trust.policy]] covers the package. verify=True against an uncovered package is a documented no-op, not enforcement.

ocx's --output mode is not wrapped: it writes the raw predicate bytes and prints no listing at all, so there is no report for a typed method to return. Reach it through ocx.invoke(["package", "sbom", "--output", "-", str(ref)]), whose stdout the SDK captures.

PARAMETER DESCRIPTION
ref

The package reference to list SBOMs for.

TYPE: PackageLike

platform

Restrict to one platform's SBOM.

TYPE: str | None DEFAULT: None

summary

Also parse each entry as CycloneDX (1.5-1.7 only) and report its component counts on SbomEntry.summary. An entry whose document will not parse moves to refused (reason_kind "sbom_summary_failed") rather than appearing with an empty summary, so this flag can change which array an entry lands in — it never empties the listing, and never touches an entry that parsed.

TYPE: bool DEFAULT: False

predicate_type

Restrict to this predicate type — ocx's --type.

TYPE: str | None DEFAULT: None

certificate_identity

The pinned keyless identity, for verifying attached signatures. Required together with certificate_oidc_issuer, and neither is usable with key.

TYPE: str | None DEFAULT: None

certificate_oidc_issuer

The pinned keyless OIDC issuer.

TYPE: str | None DEFAULT: None

key

A key reference, for key-based verification.

TYPE: str | None DEFAULT: None

signature_format

Restrict to one signature format. 'both' is write-side only — it names two shapes, and a result cannot say "either of these satisfied me".

TYPE: SignatureFormat | None DEFAULT: None

sigstore_trusted_root

A non-default Sigstore trusted root bundle.

TYPE: str | Path | None DEFAULT: None

rekor_url

A non-default Rekor instance.

TYPE: str | None DEFAULT: None

no_cache

Skip ocx's verification cache.

TYPE: bool DEFAULT: False

verify

Verify attached signatures. False names no cryptography, so it cannot be combined with key or with either certificate flag.

TYPE: bool | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
SbomListingReport

The summary, the listed entries, and anything refused.

RAISES DESCRIPTION
ValueError

The identity pair is incomplete or combined with key; verify=False was combined with key or with a certificate flag; or signature_format was 'both'.

Source code in src/ocx_sdk/_client.py
def sbom(
    self,
    ref: PackageLike,
    *,
    platform: str | None = None,
    summary: bool = False,
    predicate_type: str | None = None,
    certificate_identity: str | None = None,
    certificate_oidc_issuer: str | None = None,
    key: str | None = None,
    signature_format: SignatureFormat | None = None,
    sigstore_trusted_root: str | Path | None = None,
    rekor_url: str | None = None,
    no_cache: bool = False,
    verify: bool | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> SbomListingReport:
    """List and verify the SBOM documents attached to a package (C-014).

    A read — not mutating. Exits 0 even when candidates were refused;
    check `summary.status`, not the exit code, for a
    `"partial_failure"` listing.

    `verify` gates the same way `install`/`pull`'s does (C-008): ocx's
    default is on, but the gate only fires when a `[[trust.policy]]`
    covers the package. `verify=True` against an uncovered package is a
    documented no-op, not enforcement.

    ocx's `--output` mode is not wrapped: it writes the raw predicate
    bytes and prints no listing at all, so there is no report for a
    typed method to return. Reach it through
    `ocx.invoke(["package", "sbom", "--output", "-", str(ref)])`, whose
    stdout the SDK captures.

    Args:
        ref: The package reference to list SBOMs for.
        platform: Restrict to one platform's SBOM.
        summary: Also parse each entry as CycloneDX (1.5-1.7 only) and
            report its component counts on `SbomEntry.summary`. An
            entry whose document will not parse **moves** to `refused`
            (`reason_kind` `"sbom_summary_failed"`) rather than
            appearing with an empty summary, so this flag can change
            which array an entry lands in — it never empties the
            listing, and never touches an entry that parsed.
        predicate_type: Restrict to this predicate type — ocx's
            `--type`.
        certificate_identity: The pinned keyless identity, for
            verifying attached signatures. Required together with
            `certificate_oidc_issuer`, and neither is usable with
            `key`.
        certificate_oidc_issuer: The pinned keyless OIDC issuer.
        key: A key reference, for key-based verification.
        signature_format: Restrict to one signature format. `'both'` is
            write-side only — it names two shapes, and a result cannot
            say "either of these satisfied me".
        sigstore_trusted_root: A non-default Sigstore trusted root
            bundle.
        rekor_url: A non-default Rekor instance.
        no_cache: Skip ocx's verification cache.
        verify: Verify attached signatures. `False` names no
            cryptography, so it cannot be combined with `key` or with
            either certificate flag.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the
            config's.

    Returns:
        The summary, the listed entries, and anything refused.

    Raises:
        ValueError: The identity pair is incomplete or combined with
            `key`; `verify=False` was combined with `key` or with a
            certificate flag; or `signature_format` was `'both'`.
    """
    _identity_guards(
        certificate_identity=certificate_identity,
        certificate_oidc_issuer=certificate_oidc_issuer,
        key=key,
    )
    _read_side_format(signature_format)
    if verify is False:
        refused = sorted(
            name
            for name, value in (
                ("key", key),
                ("certificate_identity", certificate_identity),
                ("certificate_oidc_issuer", certificate_oidc_issuer),
            )
            if value is not None
        )
        if refused:
            raise ValueError(
                f"verify=False asks for no cryptography and {', '.join(refused)} names material to check "
                f"with, which ocx refuses as contradictory rather than accepting an argument it would "
                f"never use. Drop verify=False to check them, or drop them to list unverified."
            )
    command = [
        "package",
        "sbom",
        *_flag("--platform", platform),
        *_switch("--summary", summary),
        *_flag("--type", predicate_type),
        *_flag("--certificate-identity", certificate_identity),
        *_flag("--certificate-oidc-issuer", certificate_oidc_issuer),
        *_flag("--key", key),
        *_flag("--signature-format", signature_format),
        *_flag("--sigstore-trusted-root", sigstore_trusted_root),
        *_flag("--rekor-url", rekor_url),
        *_switch("--no-cache", no_cache),
        *_toggle("--verify", "--no-verify", verify),
    ]
    return SbomListingReport.from_json(self._call(command, (str(ref),), timeout=timeout, retry=retry).stdout)

copy

copy(source: PackageLike, *, to: str | None = None, identifier: str | None = None, platforms: Iterable[str] = (), cascade: bool = False, keep_tag: bool | None = None, referrers: bool | None = None, description: bool = False, annotations: Mapping[str, str] | None = None, dry_run: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CopyReport

Copy a package from one location to another, server-side (C-015).

mutating=not dry_run (D5): copy(dry_run=True) writes nothing and is exactly the read a transient registry blip should retry, so a flat True would wrongly disable retry for it. dry_run=True reports status="planned" rather than describing a copy that already happened.

Non-empty sidecar_conflicts on the result means the target already had different content under a sidecar tag copy would have written — that call exits 65 and raises. Recover the report, conflicts included, with partial_report(err) and CopyReport.from_json (D10).

to/identifier is mutual exclusion, not xor: giving neither is a legal invocation — ocx falls back to the configured default registry, keeping source's repository and tag. Only giving both is refused.

A digest-form source with no tag needs identifier (there is no tag to keep at the target otherwise) and needs exactly one entry in platforms (there is no single-platform default to fall back on).

Not guarded here, because it would take a second identifier parser: ocx refuses a target that carries no tag, whichever route produced it (package_copy.rs:110) — so a tagless source with no identifier, or a tagless identifier, exits 64. Spell the tag out at whichever end names the target.

PARAMETER DESCRIPTION
source

The package reference to copy.

TYPE: PackageLike

to

Rewrite only the target's registry host, keeping source's repository path and tag (package_copy.rs:19-24 — value_name = "REGISTRY", a host, not a full reference). Refused alongside identifier.

TYPE: str | None DEFAULT: None

identifier

The full target reference, when the repository path or tag changes too. Refused alongside to. Required when source is a bare digest.

TYPE: str | None DEFAULT: None

platforms

Restrict to these platforms. Omitted copies all of them — except when source is a bare digest, which needs exactly one.

TYPE: Iterable[str] DEFAULT: ()

cascade

Also advance the rolling tags above this version at the target.

TYPE: bool DEFAULT: False

keep_tag

Write the __ocx.keep.sha256-<hex> tag for each platform manifest at the target. None leaves ocx's default, which writes it.

TYPE: bool | None DEFAULT: None

referrers

Copy OCI referrers-API attachments. None leaves ocx's default.

TYPE: bool | None DEFAULT: None

description

Also copy the source's description metadata.

TYPE: bool DEFAULT: False

annotations

Extra OCI annotations for the copied index.

TYPE: Mapping[str, str] | None DEFAULT: None

dry_run

Report what would be copied without writing.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's (D5 resolves the mutating default per dry_run).

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CopyReport

What landed at the target, including per-platform rows and

CopyReport

blob transfer counts.

RAISES DESCRIPTION
ValueError

Both to and identifier were given; or source is a bare digest and identifier is absent, or platforms does not name exactly one entry.

DataError

sidecar_conflicts is non-empty (exit 65) — see the partial-failure note above for how to recover the report.

Source code in src/ocx_sdk/_client.py
def copy(
    self,
    source: PackageLike,
    *,
    to: str | None = None,
    identifier: str | None = None,
    platforms: Iterable[str] = (),
    cascade: bool = False,
    keep_tag: bool | None = None,
    referrers: bool | None = None,
    description: bool = False,
    annotations: Mapping[str, str] | None = None,
    dry_run: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CopyReport:
    """Copy a package from one location to another, server-side (C-015).

    `mutating=not dry_run` (D5): `copy(dry_run=True)` writes nothing
    and is exactly the read a transient registry blip should retry, so
    a flat `True` would wrongly disable retry for it. `dry_run=True`
    reports `status="planned"` rather than describing a copy that
    already happened.

    Non-empty `sidecar_conflicts` on the result means the target
    already had different content under a sidecar tag `copy` would have
    written — that call exits 65 and raises. Recover the report,
    conflicts included, with `partial_report(err)` and
    `CopyReport.from_json` (D10).

    `to`/`identifier` is mutual exclusion, **not** xor: giving neither
    is a legal invocation — ocx falls back to the configured default
    registry, keeping `source`'s repository and tag. Only giving both
    is refused.

    A digest-form `source` with no tag needs `identifier` (there is no
    tag to keep at the target otherwise) and needs exactly one entry in
    `platforms` (there is no single-platform default to fall back on).

    Not guarded here, because it would take a second identifier parser:
    ocx refuses a **target** that carries no tag, whichever route
    produced it (`package_copy.rs:110`) — so a tagless `source` with no
    `identifier`, or a tagless `identifier`, exits 64. Spell the tag out
    at whichever end names the target.

    Args:
        source: The package reference to copy.
        to: Rewrite only the target's registry host, keeping `source`'s
            repository path and tag (`package_copy.rs:19-24` —
            `value_name = "REGISTRY"`, a host, not a full reference).
            Refused alongside `identifier`.
        identifier: The full target reference, when the repository path
            or tag changes too. Refused alongside `to`. Required when
            `source` is a bare digest.
        platforms: Restrict to these platforms. Omitted copies all of
            them — except when `source` is a bare digest, which needs
            exactly one.
        cascade: Also advance the rolling tags above this version at
            the target.
        keep_tag: Write the `__ocx.keep.sha256-<hex>` tag for each platform
            manifest at the target. `None` leaves ocx's default, which
            writes it.
        referrers: Copy OCI referrers-API attachments. `None` leaves
            ocx's default.
        description: Also copy the source's description metadata.
        annotations: Extra OCI annotations for the copied index.
        dry_run: Report what would be copied without writing.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the
            config's (D5 resolves the mutating default per `dry_run`).

    Returns:
        What landed at the target, including per-platform rows and
        blob transfer counts.

    Raises:
        ValueError: Both `to` and `identifier` were given; or `source`
            is a bare digest and `identifier` is absent, or `platforms`
            does not name exactly one entry.
        DataError: `sidecar_conflicts` is non-empty (exit 65) — see the
            partial-failure note above for how to recover the report.
    """
    if to is not None and identifier is not None:
        raise ValueError(
            "to rewrites the registry host and identifier states the whole target reference, so ocx "
            "refuses both: identifier already says everything to would have changed. Pass one of them, "
            "or neither to keep source's repository and tag at the configured default registry."
        )
    wanted = tuple(platforms)
    if _digest_without_tag(str(source)):
        if len(wanted) != 1:
            raise ValueError(
                f"{source} names a manifest by digest, and a leaf manifest carries no platform of its "
                f"own, so exactly one platform must be declared; {len(wanted)} were given. Pass one "
                f"platform, or name the source by tag instead."
            )
        if identifier is None:
            raise ValueError(
                f"{source} names a manifest by digest, which carries no tag for the target to keep, so "
                f"ocx needs the target spelled out. Pass identifier with the full target reference."
            )
    command = [
        "package",
        "copy",
        *_flag("--to", to),
        *_flag("--identifier", identifier),
        *_repeated("--platform", wanted),
        *_switch("--cascade", cascade),
        *_toggle("--keep-tag", "--no-keep-tag", keep_tag),
        *_toggle("--referrers", "--no-referrers", referrers),
        *_switch("--description", description),
        *_repeated("--annotation", (f"{name}={value}" for name, value in (annotations or {}).items())),
        *_switch("--dry-run", dry_run),
    ]
    result = self._call(command, (str(source),), timeout=timeout, retry=retry, mutating=not dry_run)
    return CopyReport.from_json(result.stdout)

announce

announce(package: str, *, tags: Iterable[str] = (), tags_file: str | Path | None = None, tags_from_registry: bool = False, refresh: bool = False, index_repo: str | None = None, forge: Forge | None = None, transport: Transport | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> AnnounceReport

Publish a package's curated tag set into the index (C-061).

Re-observes the named tags on the registry, rebuilds the index entry and opens — or updates — a pull or merge request against the index repository. mutating=True (D5): retries are off by default. A run that changes nothing reports status="unchanged" and commits nothing.

Needs a forge credential: OcxConfig.forge_token (or an ambient OCX_ANNOUNCE_TOKEN), or the job token under transport="git" inside a GitLab job. Without one ocx exits 80 before touching the network. HostEnv.minimal() drops the whole CI identity ladder, so a hermetic handle has to name the token in its config.

Exactly one tag source is required and ocx enforces it (exit 64): tags replaces the curated set, the other three add to it, and they are mutually exclusive. Nothing is checked here — clap's usage error is the category, and it names the rule.

ocx's --yank/--unyank/--fork/--out are not wrapped; reach them through invoke.

PARAMETER DESCRIPTION
package

The package to announce, as <namespace>/<package>.

TYPE: str

tags

Replace the curated tag set with these. A committed tag not named here is dropped; a reserved __ocx or legacy sha256.<hex> tag named here is dropped and reported.

TYPE: Iterable[str] DEFAULT: ()

tags_file

Add the tags listed in this file — the one cascade_repair(announce_tags=...) wrote, typically.

TYPE: str | Path | None DEFAULT: None

tags_from_registry

Add every tag the registry repository currently holds.

TYPE: bool DEFAULT: False

refresh

Re-observe every committed tag, picking up a digest that moved, without changing which tags are curated.

TYPE: bool DEFAULT: False

index_repo

The index repository, as [HOST/]NAMESPACE/PROJECT. None takes ocx's default, ocx-sh/index.

TYPE: str | None DEFAULT: None

forge

Which forge hosts it. Inferred for github.com and gitlab.com; required for a self-hosted host.

TYPE: Forge | None DEFAULT: None

transport

"api" (ocx's default) or "git", the only way a GitLab job token can open a merge request.

TYPE: Transport | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
AnnounceReport

What reached the index: status, request URL, capability checks.

RAISES DESCRIPTION
AuthError

No forge credential resolved (exit 80).

UsageError

No tag source, more than one, or a self-hosted index_repo without forge (exit 64).

ForgeCapabilityUnavailableError

The forge refused a job-token push (exit 86).

Source code in src/ocx_sdk/_client.py
def announce(
    self,
    package: str,
    *,
    tags: Iterable[str] = (),
    tags_file: str | Path | None = None,
    tags_from_registry: bool = False,
    refresh: bool = False,
    index_repo: str | None = None,
    forge: Forge | None = None,
    transport: Transport | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> AnnounceReport:
    """Publish a package's curated tag set into the index (C-061).

    Re-observes the named tags on the registry, rebuilds the index entry
    and opens — or updates — a pull or merge request against the index
    repository. `mutating=True` (D5): retries are off by default. A run
    that changes nothing reports `status="unchanged"` and commits nothing.

    Needs a forge credential: `OcxConfig.forge_token` (or an ambient
    `OCX_ANNOUNCE_TOKEN`), or the job token under `transport="git"`
    inside a GitLab job. Without one ocx exits 80 before touching the
    network. `HostEnv.minimal()` drops the whole CI identity ladder, so a
    hermetic handle has to name the token in its config.

    Exactly one tag source is required and ocx enforces it (exit 64):
    `tags` replaces the curated set, the other three add to it, and they
    are mutually exclusive. Nothing is checked here — clap's usage error
    is the category, and it names the rule.

    ocx's `--yank`/`--unyank`/`--fork`/`--out` are not wrapped; reach them
    through `invoke`.

    Args:
        package: The package to announce, as `<namespace>/<package>`.
        tags: **Replace** the curated tag set with these. A committed tag
            not named here is dropped; a reserved `__ocx` or legacy
            `sha256.<hex>` tag named here is dropped and reported.
        tags_file: **Add** the tags listed in this file — the one
            `cascade_repair(announce_tags=...)` wrote, typically.
        tags_from_registry: **Add** every tag the registry repository
            currently holds.
        refresh: Re-observe every committed tag, picking up a digest that
            moved, without changing which tags are curated.
        index_repo: The index repository, as `[HOST/]NAMESPACE/PROJECT`.
            `None` takes ocx's default, `ocx-sh/index`.
        forge: Which forge hosts it. Inferred for github.com and
            gitlab.com; required for a self-hosted host.
        transport: `"api"` (ocx's default) or `"git"`, the only way a
            GitLab job token can open a merge request.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries.

    Returns:
        What reached the index: status, request URL, capability checks.

    Raises:
        AuthError: No forge credential resolved (exit 80).
        UsageError: No tag source, more than one, or a self-hosted
            `index_repo` without `forge` (exit 64).
        ForgeCapabilityUnavailableError: The forge refused a job-token
            push (exit 86).
    """
    command = [
        "package",
        "announce",
        *_repeated("--tags", tags),
        *_flag("--tags-file", tags_file),
        *_switch("--tags-from-registry", tags_from_registry),
        *_switch("--refresh", refresh),
        *_forge_flags(index_repo, forge, transport),
    ]
    result = self._call(command, (package,), timeout=timeout, retry=retry, mutating=True)
    return AnnounceReport.from_json(result.stdout)

claim

claim(package: str, *, repository: str, owners: Iterable[str] = (), upstream_org: str | None = None, upstream_repository_url: str | None = None, upstream_disclaimer: str | None = None, index_repo: str | None = None, forge: Forge | None = None, transport: Transport | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> ClaimReport

Claim a package in the index so its tags can be announced (C-060).

Renders the package's index entry and opens a pull or merge request. mutating=True (D5): retries are off by default. Needs the same forge credential announce does.

A package that is already claimed is refused with exit 65 and an error envelope — not a report, and not a distinguishable subclass: the envelope carries no detail for it, and the SDK never classifies by message text. An idempotent CI step therefore reads as except DataError, with error_envelope(err) as the machine handle on what ocx said.

ocx's --fork/--out are not wrapped; reach them through invoke.

PARAMETER DESCRIPTION
package

The package to claim, as <namespace>/<package>.

TYPE: str

repository

The physical OCI repository the bytes live in, as oci://HOST/PATH — what every later announce resolves tags against.

TYPE: str

owners

Owners as LOGIN or LOGIN:ID, in the order to record. Giving any replaces the detected list; the invoking identity is not added. A bare LOGIN is resolved against the forge's users API.

TYPE: Iterable[str] DEFAULT: ()

upstream_org

The upstream organization a third-party package mirrors. Anchors the other two upstream_* arguments — ocx refuses either without it.

TYPE: str | None DEFAULT: None

upstream_repository_url

The upstream repository, as an absolute http/https URL with no embedded credentials.

TYPE: str | None DEFAULT: None

upstream_disclaimer

A disclaimer recorded on the index entry.

TYPE: str | None DEFAULT: None

index_repo

The index repository, as [HOST/]NAMESPACE/PROJECT.

TYPE: str | None DEFAULT: None

forge

Which forge hosts it; required for a self-hosted host.

TYPE: Forge | None DEFAULT: None

transport

"api" (ocx's default) or "git".

TYPE: Transport | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. Defaults to no retries.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
ClaimReport

The rendered entry: status, owners, request URL, capability checks.

RAISES DESCRIPTION
AuthError

No forge credential resolved (exit 80).

DataError

The package is already claimed (exit 65).

UsageError

A self-hosted index_repo without forge, or an upstream_* argument without upstream_org (exit 64).

Source code in src/ocx_sdk/_client.py
def claim(
    self,
    package: str,
    *,
    repository: str,
    owners: Iterable[str] = (),
    upstream_org: str | None = None,
    upstream_repository_url: str | None = None,
    upstream_disclaimer: str | None = None,
    index_repo: str | None = None,
    forge: Forge | None = None,
    transport: Transport | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> ClaimReport:
    """Claim a package in the index so its tags can be announced (C-060).

    Renders the package's index entry and opens a pull or merge request.
    `mutating=True` (D5): retries are off by default. Needs the same forge
    credential `announce` does.

    **A package that is already claimed is refused** with exit 65 and an
    error envelope — not a report, and not a distinguishable subclass:
    the envelope carries no `detail` for it, and the SDK never classifies
    by message text. An idempotent CI step therefore reads as
    `except DataError`, with `error_envelope(err)` as the machine handle
    on what ocx said.

    ocx's `--fork`/`--out` are not wrapped; reach them through `invoke`.

    Args:
        package: The package to claim, as `<namespace>/<package>`.
        repository: The physical OCI repository the bytes live in, as
            `oci://HOST/PATH` — what every later `announce` resolves
            tags against.
        owners: Owners as `LOGIN` or `LOGIN:ID`, in the order to record.
            Giving any **replaces** the detected list; the invoking
            identity is not added. A bare `LOGIN` is resolved against
            the forge's users API.
        upstream_org: The upstream organization a third-party package
            mirrors. Anchors the other two `upstream_*` arguments —
            ocx refuses either without it.
        upstream_repository_url: The upstream repository, as an absolute
            `http`/`https` URL with no embedded credentials.
        upstream_disclaimer: A disclaimer recorded on the index entry.
        index_repo: The index repository, as `[HOST/]NAMESPACE/PROJECT`.
        forge: Which forge hosts it; required for a self-hosted host.
        transport: `"api"` (ocx's default) or `"git"`.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. Defaults to no retries.

    Returns:
        The rendered entry: status, owners, request URL, capability checks.

    Raises:
        AuthError: No forge credential resolved (exit 80).
        DataError: The package is already claimed (exit 65).
        UsageError: A self-hosted `index_repo` without `forge`, or an
            `upstream_*` argument without `upstream_org` (exit 64).
    """
    command = [
        "package",
        "claim",
        "--repository",
        repository,
        *_repeated("--owner", owners),
        *_flag("--upstream-org", upstream_org),
        *_flag("--upstream-repository-url", upstream_repository_url),
        *_flag("--upstream-disclaimer", upstream_disclaimer),
        *_forge_flags(index_repo, forge, transport),
    ]
    result = self._call(command, (package,), timeout=timeout, retry=retry, mutating=True)
    return ClaimReport.from_json(result.stdout)

receipt

receipt(bundle: str | Path, *, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> BuildReceipt | None

Print the build receipt create wrote beside a bundle, if any.

Answers what push and test would fall back to for the flags a caller leaves out. Local and read-only — it touches no registry — so it keeps the session retry policy.

PARAMETER DESCRIPTION
bundle

The bundle archive create wrote.

TYPE: str | Path

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
BuildReceipt | None

The receipt, or None when no receipt sits beside the bundle —

BuildReceipt | None

ocx's exit 79, the ordinary state for a bundle handed over from

BuildReceipt | None

elsewhere, rather than an error here.

RAISES DESCRIPTION
DataError

A receipt file that ocx cannot read (exit 65).

Source code in src/ocx_sdk/_client.py
def receipt(
    self,
    bundle: str | Path,
    *,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> BuildReceipt | None:
    """Print the build receipt `create` wrote beside a bundle, if any.

    Answers what `push` and `test` would fall back to for the flags a
    caller leaves out. Local and read-only — it touches no registry — so
    it keeps the session retry policy.

    Args:
        bundle: The bundle archive `create` wrote.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The receipt, or `None` when no receipt sits beside the bundle —
        ocx's exit 79, the ordinary state for a bundle handed over from
        elsewhere, rather than an error here.

    Raises:
        DataError: A receipt file that ocx cannot read (exit 65).
    """
    result = self._call(["package", "receipt"], [str(bundle)], timeout=timeout, retry=retry, ok_codes=_RECEIPT_OK)
    if result.exit_code == ExitCode.NOT_FOUND:
        return None
    return BuildReceipt.from_json(result.stdout)

cascade_check

cascade_check(*refs: PackageLike, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CascadeCheckReport

Report where packages' rolling tags disagree with their versions.

Read-only: authenticates for pull only and writes nothing, so it keeps the session retry policy. Report-then-fail: a finding makes ocx exit 65 with the report on stdout, and that is a result here — report.clean (the exit code's answer) says whether anything disagreed, and the rows say what. The same code with no report is still the failure it names.

PARAMETER DESCRIPTION
*refs

Packages to audit. A tag (cmake:3.28) narrows the audit to that part of the graph.

TYPE: PackageLike DEFAULT: ()

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CascadeCheckReport

One audit per package, carrying the exit code.

RAISES DESCRIPTION
UsageError

A package names a digest, or a tag that is not a version (exit 64).

DataError

Exit 65 without a report — a fault, not a finding.

Source code in src/ocx_sdk/_client.py
def cascade_check(
    self,
    *refs: PackageLike,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CascadeCheckReport:
    """Report where packages' rolling tags disagree with their versions.

    Read-only: authenticates for pull only and writes nothing, so it
    keeps the session retry policy. **Report-then-fail**: a finding makes
    ocx exit 65 *with* the report on stdout, and that is a result here
    — `report.clean` (the exit code's answer) says whether anything
    disagreed, and the rows say what. The same code with no report is
    still the failure it names.

    Args:
        *refs: Packages to audit. A tag (`cmake:3.28`) narrows the audit
            to that part of the graph.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        One audit per package, carrying the exit code.

    Raises:
        UsageError: A package names a digest, or a tag that is not a
            version (exit 64).
        DataError: Exit 65 *without* a report — a fault, not a finding.
    """
    result = self._call(
        ["package", "cascade", "check"], _identifiers(refs), timeout=timeout, retry=retry, ok_codes=_CASCADE_OK
    )
    return CascadeCheckReport.from_json(tolerated_report(result), exit_code=result.exit_code)

cascade_repair

cascade_repair(*refs: PackageLike, dry_run: bool = False, announce_tags: str | Path | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CascadeRepairReport

Re-point packages' rolling tags at the content their versions imply.

Publishes nothing new — every index it writes references content the registry already serves. mutating=not dry_run (D5), as copy: the preview writes nothing and keeps the session retry policy.

Report-then-fail like cascade_check: exit 65 with the report when a finding remains — on a dry_run, having planned anything is the finding — so report.clean is the answer, not an exception.

Repairing the registry does not update the public index. Pass announce_tags to record the tags this run moved, then hand that file to announce(..., tags_file=...).

PARAMETER DESCRIPTION
*refs

Packages to repair.

TYPE: PackageLike DEFAULT: ()

dry_run

Compute and report the plan without writing.

TYPE: bool DEFAULT: False

announce_tags

Write the rolling tags this run moved or created to this file, one per line. Takes one package per run — ocx exits 64 when it is given more.

TYPE: str | Path | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's — which D5 resolves to no retries unless dry_run.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CascadeRepairReport

One entry per package, carrying the exit code.

RAISES DESCRIPTION
UsageError

A package names a digest or a non-version tag, or announce_tags was given with more than one package (exit 64).

DataError

Exit 65 without a report.

Source code in src/ocx_sdk/_client.py
def cascade_repair(
    self,
    *refs: PackageLike,
    dry_run: bool = False,
    announce_tags: str | Path | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CascadeRepairReport:
    """Re-point packages' rolling tags at the content their versions imply.

    Publishes nothing new — every index it writes references content the
    registry already serves. `mutating=not dry_run` (D5), as `copy`:
    the preview writes nothing and keeps the session retry policy.

    **Report-then-fail** like `cascade_check`: exit 65 with the report
    when a finding remains — on a `dry_run`, having planned anything is
    the finding — so `report.clean` is the answer, not an exception.

    Repairing the registry does not update the public index. Pass
    `announce_tags` to record the tags this run moved, then hand that
    file to `announce(..., tags_file=...)`.

    Args:
        *refs: Packages to repair.
        dry_run: Compute and report the plan without writing.
        announce_tags: Write the rolling tags this run moved or created
            to this file, one per line. Takes one package per run — ocx
            exits 64 when it is given more.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's
            — which D5 resolves to no retries unless `dry_run`.

    Returns:
        One entry per package, carrying the exit code.

    Raises:
        UsageError: A package names a digest or a non-version tag, or
            `announce_tags` was given with more than one package (exit 64).
        DataError: Exit 65 *without* a report.
    """
    command = [
        "package",
        "cascade",
        "repair",
        *_switch("--dry-run", dry_run),
        *_flag("--announce-tags", announce_tags),
    ]
    result = self._call(
        command, _identifiers(refs), timeout=timeout, retry=retry, mutating=not dry_run, ok_codes=_CASCADE_OK
    )
    return CascadeRepairReport.from_json(tolerated_report(result), exit_code=result.exit_code)

PatchCommands dataclass

PatchCommands(_runner: _Runner)

The ocx patch command group.

sync

sync(*, platform: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CommandResult

Refresh patch descriptors and companion packages.

Re-checks every installed package, including ones installed before patches were configured, and needs network access. Returns the raw result: no payload shape is pinned for this command yet.

PARAMETER DESCRIPTION
platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The exit code and whatever ocx printed.

Source code in src/ocx_sdk/_client.py
def sync(
    self,
    *,
    platform: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> CommandResult:
    """Refresh patch descriptors and companion packages.

    Re-checks every installed package, including ones installed before
    patches were configured, and needs network access. Returns the raw
    result: no payload shape is pinned for this command yet.

    Args:
        platform: The platform to resolve against.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The exit code and whatever ocx printed.
    """
    command = ["patch", "sync", *_flag("--platform", platform)]
    return self._runner.typed(command, timeout=timeout, retry=retry)

Project dataclass

Project(_runner: _Runner, path: Path)

A project-tier handle: every call carries --project <file>.

Obtained from Ocx.project(path), never constructed directly. Because the path travels explicitly on every call, no method depends on the working directory, and an ambient OCX_PROJECT can never retarget one. init is the exception, and only because ocx's is: it takes no flags and writes into the working directory, so the handle sets that instead.

Example
from ocx_sdk import Ocx

project = Ocx().project("/srv/build")
project.add("ocx.sh/go-task/task:3", group="ci")
project.lock()
report = project.env()
environment = report.compose().mapping
ATTRIBUTE DESCRIPTION
path

The absolute project file — the ocx.toml itself, which is what ocx's --project names.

TYPE: Path

session_config

The OcxConfig every call spawns under.

TYPE: OcxConfig

session_config property

session_config: OcxConfig

The OcxConfig this handle spawns under — see Ocx.session_config.

with_config

with_config(**overrides: Unpack[ConfigOverrides]) -> Project

Derive a project handle with some configuration fields replaced.

PARAMETER DESCRIPTION
**overrides

OcxConfig field names and their new values, as ConfigOverrides spells them.

TYPE: Unpack[ConfigOverrides] DEFAULT: {}

RETURNS DESCRIPTION
Project

The same project, seen through a derived Ocx.

RAISES DESCRIPTION
TypeError

An override names no OcxConfig field.

Source code in src/ocx_sdk/_client.py
def with_config(self, **overrides: Unpack[ConfigOverrides]) -> Project:
    """Derive a project handle with some configuration fields replaced.

    Args:
        **overrides: `OcxConfig` field names and their new values, as
            `ConfigOverrides` spells them.

    Returns:
        The same project, seen through a derived `Ocx`.

    Raises:
        TypeError: An override names no `OcxConfig` field.
    """
    return Project(self._runner.derive(**overrides), self.path)

init

init(*, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CommandResult

Create a minimal ocx.toml in the project directory.

The one project-tier call that carries no --project: ocx init takes no flags and writes into the working directory, and naming a project would ask ocx to resolve the very file the call exists to create. This runs it with the project's directory as its cwd instead, so the file lands where the handle points rather than wherever the caller happens to be.

Returns the raw result: ocx pins no JSON payload for init, so there is nothing to type yet.

PARAMETER DESCRIPTION
timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The exit code and whatever ocx printed.

Source code in src/ocx_sdk/_client.py
def init(self, *, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> CommandResult:
    """Create a minimal `ocx.toml` in the project directory.

    The one project-tier call that carries no `--project`: `ocx init`
    takes no flags and writes into the working directory, and naming a
    project would ask ocx to resolve the very file the call exists to
    create. This runs it with the project's directory as its `cwd`
    instead, so the file lands where the handle points rather than
    wherever the caller happens to be.

    Returns the raw result: ocx pins no JSON payload for `init`, so there
    is nothing to type yet.

    Args:
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The exit code and whatever ocx printed.
    """
    return self._runner.typed(("init",), timeout=timeout, retry=retry, cwd=self.path.parent)

add

add(*refs: PackageLike, group: str | None = None, pull: bool | None = None, platform: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[ToolRow, ...]

Add tool bindings to ocx.toml.

PARAMETER DESCRIPTION
*refs

Identifiers, each optionally prefixed NAME=.

TYPE: PackageLike DEFAULT: ()

group

The group to add to. None uses the implicit [tools] table.

TYPE: str | None DEFAULT: None

pull

Materialize the resolved packages. None leaves the choice to ocx's own default.

TYPE: bool | None DEFAULT: None

platform

The platform to resolve against, as os/arch[/variant][+feature].

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[ToolRow, ...]

The lock rows the call wrote.

Source code in src/ocx_sdk/_client.py
def add(
    self,
    *refs: PackageLike,
    group: str | None = None,
    pull: bool | None = None,
    platform: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[ToolRow, ...]:
    """Add tool bindings to `ocx.toml`.

    Args:
        *refs: Identifiers, each optionally prefixed `NAME=`.
        group: The group to add to. `None` uses the implicit `[tools]`
            table.
        pull: Materialize the resolved packages. `None` leaves the
            choice to ocx's own default.
        platform: The platform to resolve against, as
            `os/arch[/variant][+feature]`.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The lock rows the call wrote.
    """
    command = [
        "add",
        *_flag("--group", group),
        *_toggle("--pull", "--no-pull", pull),
        *_flag("--platform", platform),
    ]
    return parse_tool_rows(self._call(command, _identifiers(refs), timeout=timeout, retry=retry).stdout)

remove

remove(*names: PackageLike, group: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[ToolRow, ...]

Remove tool bindings from ocx.toml.

PARAMETER DESCRIPTION
*names

Binding names or fully-qualified identifiers.

TYPE: PackageLike DEFAULT: ()

group

The group to target. None searches every group and fails when a name appears in more than one.

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[ToolRow, ...]

The lock rows the call wrote — empty when nothing changed.

Source code in src/ocx_sdk/_client.py
def remove(
    self,
    *names: PackageLike,
    group: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[ToolRow, ...]:
    """Remove tool bindings from `ocx.toml`.

    Args:
        *names: Binding names or fully-qualified identifiers.
        group: The group to target. `None` searches every group and
            fails when a name appears in more than one.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The lock rows the call wrote — empty when nothing changed.
    """
    command = ["remove", *_flag("--group", group)]
    return parse_tool_rows(self._call(command, _identifiers(names), timeout=timeout, retry=retry).stdout)

lock

lock(*, check_only: bool = False, pull: bool | None = None, platform: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[ToolRow, ...] | None

Resolve declared tags to digests and write ocx.lock.

PARAMETER DESCRIPTION
check_only

Verify the lock is current and write nothing — ocx's --check. Drift raises DataError; a missing lock raises ConfigError. Named apart from the check= on run, which is the subprocess.run sense of "raise on a non-zero exit".

TYPE: bool DEFAULT: False

pull

Materialize the resolved packages. None leaves the choice to ocx's own default.

TYPE: bool | None DEFAULT: None

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[ToolRow, ...] | None

The lock rows written, or None under check_only — ocx reports

tuple[ToolRow, ...] | None

a current lock with exit 0 and an empty body.

Source code in src/ocx_sdk/_client.py
def lock(
    self,
    *,
    check_only: bool = False,
    pull: bool | None = None,
    platform: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[ToolRow, ...] | None:
    """Resolve declared tags to digests and write `ocx.lock`.

    Args:
        check_only: Verify the lock is current and write nothing — ocx's
            `--check`. Drift raises `DataError`; a missing lock raises
            `ConfigError`. Named apart from the `check=` on `run`, which
            is the `subprocess.run` sense of "raise on a non-zero exit".
        pull: Materialize the resolved packages. `None` leaves the
            choice to ocx's own default.
        platform: The platform to resolve against.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The lock rows written, or `None` under `check_only` — ocx reports
        a current lock with exit 0 and an empty body.
    """
    command = [
        "lock",
        *_switch("--check", check_only),
        *_toggle("--pull", "--no-pull", pull),
        *_flag("--platform", platform),
    ]
    return _rows_or_none(self._call(command, timeout=timeout, retry=retry).stdout)

update

update(*names: str, check_only: bool = False, groups: Iterable[str] = (), pull: bool | None = None, platform: str | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[ToolRow, ...] | None

Re-resolve declared tags against the registry.

PARAMETER DESCRIPTION
*names

Binding names to advance; every other pin freezes. Omitted advances the whole file.

TYPE: str DEFAULT: ()

check_only

Compare the candidate lock to its predecessor and write nothing — ocx's --check. A changed pin raises DataError.

TYPE: bool DEFAULT: False

groups

Groups to advance. default is the implicit [tools] table, all expands to every group.

TYPE: Iterable[str] DEFAULT: ()

pull

Materialize the resolved packages. None leaves the choice to ocx's own default.

TYPE: bool | None DEFAULT: None

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
tuple[ToolRow, ...] | None

The lock rows written, or None under check_only.

Source code in src/ocx_sdk/_client.py
def update(
    self,
    *names: str,
    check_only: bool = False,
    groups: Iterable[str] = (),
    pull: bool | None = None,
    platform: str | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> tuple[ToolRow, ...] | None:
    """Re-resolve declared tags against the registry.

    Args:
        *names: Binding names to advance; every other pin freezes.
            Omitted advances the whole file.
        check_only: Compare the candidate lock to its predecessor and
            write nothing — ocx's `--check`. A changed pin raises
            `DataError`.
        groups: Groups to advance. `default` is the implicit `[tools]`
            table, `all` expands to every group.
        pull: Materialize the resolved packages. `None` leaves the
            choice to ocx's own default.
        platform: The platform to resolve against.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The lock rows written, or `None` under `check_only`.
    """
    command = [
        "update",
        *_switch("--check", check_only),
        *_repeated("--group", groups),
        *_toggle("--pull", "--no-pull", pull),
        *_flag("--platform", platform),
    ]
    return _rows_or_none(self._call(command, tuple(names), timeout=timeout, retry=retry).stdout)

pull

pull(*, dry_run: Literal[True], groups: Iterable[str] = (), platform: str | None = None, lazy_mode: LazyMode | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> tuple[DryRunEntry, ...]
pull(*, dry_run: Literal[False] = False, groups: Iterable[str] = (), platform: str | None = None, lazy_mode: LazyMode | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> PullReport
pull(*, dry_run: bool = False, groups: Iterable[str] = (), platform: str | None = None, lazy_mode: LazyMode | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> PullReport | tuple[DryRunEntry, ...]

Pre-warm the object store from ocx.lock.

PARAMETER DESCRIPTION
dry_run

Report what would be fetched without writing. Changes the return type: ocx previews with a different upstream report at a different JSON root, so this answers a tuple of DryRunEntry rows rather than a PullReport.

TYPE: bool DEFAULT: False

groups

Groups to restrict the pull to.

TYPE: Iterable[str] DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
PullReport | tuple[DryRunEntry, ...]

The materialized paths and any advisories — or, under dry_run,

PullReport | tuple[DryRunEntry, ...]

one row per package describing what a real pull would do.

Source code in src/ocx_sdk/_client.py
def pull(
    self,
    *,
    dry_run: bool = False,
    groups: Iterable[str] = (),
    platform: str | None = None,
    lazy_mode: LazyMode | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> PullReport | tuple[DryRunEntry, ...]:
    """Pre-warm the object store from `ocx.lock`.

    Args:
        dry_run: Report what would be fetched without writing. Changes
            the return type: ocx previews with a different upstream
            report at a different JSON root, so this answers a tuple of
            `DryRunEntry` rows rather than a `PullReport`.
        groups: Groups to restrict the pull to.
        platform: The platform to resolve against.
        lazy_mode: When content downloads — now, or on first use.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The materialized paths and any advisories — or, under `dry_run`,
        one row per package describing what a real pull would do.
    """
    command = [
        "pull",
        *_switch("--dry-run", dry_run),
        *_repeated("--group", groups),
        *_flag("--platform", platform),
        *_flag("--lazy-mode", lazy_mode),
    ]
    stdout = self._call(command, timeout=timeout, retry=retry).stdout
    return parse_pull_dry_run(stdout) if dry_run else PullReport.from_json(stdout)

status

status(*, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> StatusReport

Report what ocx.toml and ocx.lock declare, resolving nothing.

A broken or stale lock is payload here, not failure: the call still exits 0 and the state shows up on the report.

PARAMETER DESCRIPTION
timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
StatusReport

The declared toolchain and the lock's state.

Source code in src/ocx_sdk/_client.py
def status(self, *, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> StatusReport:
    """Report what `ocx.toml` and `ocx.lock` declare, resolving nothing.

    A broken or stale lock is payload here, not failure: the call still
    exits 0 and the state shows up on the report.

    Args:
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The declared toolchain and the lock's state.
    """
    return StatusReport.from_json(self._call(("status",), timeout=timeout, retry=retry).stdout)

inspect

inspect(*names: str, groups: Iterable[str] = (), platform: str | None = None, env: Mapping[str, EnvValue] | None = None, resolve: bool = False, closure: bool = False, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> InspectReport

Inspect the toolchain's bindings and their resolution.

PARAMETER DESCRIPTION
*names

Binding names to inspect. Omitted inspects everything.

TYPE: str DEFAULT: ()

groups

Groups to restrict to.

TYPE: Iterable[str] DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

env

Extra [env] entries for this call, as KEY[:TYPE[:SEP]]=VALUE flags.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

resolve

Add the pinned identifier, digest, layers, and the resolution chain.

TYPE: bool DEFAULT: False

closure

Add the dependency closure, its surface, and any conflicts. Conflicting entrypoints raise DataError with the payload attached to the error's stderr.

TYPE: bool DEFAULT: False

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
InspectReport

The inspected packages and the composed [env] entries.

Source code in src/ocx_sdk/_client.py
def inspect(
    self,
    *names: str,
    groups: Iterable[str] = (),
    platform: str | None = None,
    env: Mapping[str, EnvValue] | None = None,
    resolve: bool = False,
    closure: bool = False,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> InspectReport:
    """Inspect the toolchain's bindings and their resolution.

    Args:
        *names: Binding names to inspect. Omitted inspects everything.
        groups: Groups to restrict to.
        platform: The platform to resolve against.
        env: Extra `[env]` entries for this call, as
            `KEY[:TYPE[:SEP]]=VALUE` flags.
        resolve: Add the pinned identifier, digest, layers, and the
            resolution chain.
        closure: Add the dependency closure, its surface, and any
            conflicts. Conflicting entrypoints raise `DataError` with
            the payload attached to the error's stderr.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The inspected packages and the composed `[env]` entries.
    """
    command = [
        "inspect",
        *_repeated("--group", groups),
        *_flag("--platform", platform),
        *_switch("--resolve", resolve),
        *_switch("--closure", closure),
        *_env_flags(env),
    ]
    return InspectReport.from_json(self._call(command, tuple(names), timeout=timeout, retry=retry).stdout)

env

env(*, groups: Iterable[str] = (), platform: str | None = None, env: Mapping[str, EnvValue] | None = None, pull: bool | None = None, lazy_mode: LazyMode | None = None, show_patches: bool = False, pinned: bool | None = None, timeout: MaybeTimeout = UNSET, retry: MaybeRetry = UNSET) -> EnvReport

Report the environment the toolchain composes.

The report carries this handle's host snapshot, so compose() stays hermetic: a report produced under HostEnv.clean() composes against nothing ambient unless you pass a different base.

Note that ocx's env command takes no binding names — narrow with groups instead.

PARAMETER DESCRIPTION
groups

Groups to compose. Omitted composes the default set.

TYPE: Iterable[str] DEFAULT: ()

platform

The platform to resolve against.

TYPE: str | None DEFAULT: None

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

pull

Materialize missing content first. None leaves the choice to ocx's own default.

TYPE: bool | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

show_patches

Include patch-contributed entries.

TYPE: bool DEFAULT: False

pinned

Resolve through the rendered toolchain's pinned digests rather than following its links — ocx's --pinned / --no-pinned. None leaves the choice to ocx.toml's pinned key, then OCX_TOOLCHAIN_PINNED, then ocx's default of following the links.

TYPE: bool | None DEFAULT: None

timeout

Seconds per attempt. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

retry

Retry policy. None opts out; omitted takes the config's.

TYPE: MaybeRetry DEFAULT: UNSET

RETURNS DESCRIPTION
EnvReport

The typed entries plus the binaries, entrypoints, integrations,

EnvReport

and advisories envelope.

Source code in src/ocx_sdk/_client.py
def env(
    self,
    *,
    groups: Iterable[str] = (),
    platform: str | None = None,
    env: Mapping[str, EnvValue] | None = None,
    pull: bool | None = None,
    lazy_mode: LazyMode | None = None,
    show_patches: bool = False,
    pinned: bool | None = None,
    timeout: MaybeTimeout = UNSET,
    retry: MaybeRetry = UNSET,
) -> EnvReport:
    """Report the environment the toolchain composes.

    The report carries this handle's host snapshot, so `compose()` stays
    hermetic: a report produced under `HostEnv.clean()` composes against
    nothing ambient unless you pass a different base.

    Note that ocx's `env` command takes no binding names — narrow with
    `groups` instead.

    Args:
        groups: Groups to compose. Omitted composes the default set.
        platform: The platform to resolve against.
        env: Extra `[env]` entries for this call.
        pull: Materialize missing content first. `None` leaves the
            choice to ocx's own default.
        lazy_mode: When content downloads — now, or on first use.
        show_patches: Include patch-contributed entries.
        pinned: Resolve through the rendered toolchain's pinned digests
            rather than following its links — ocx's `--pinned` /
            `--no-pinned`. `None` leaves the choice to `ocx.toml`'s
            `pinned` key, then `OCX_TOOLCHAIN_PINNED`, then ocx's
            default of following the links.
        timeout: Seconds per attempt. Omitted takes the config's.
        retry: Retry policy. `None` opts out; omitted takes the config's.

    Returns:
        The typed entries plus the binaries, entrypoints, integrations,
        and advisories envelope.
    """
    command = [
        "env",
        *_repeated("--group", groups),
        *_flag("--platform", platform),
        *_toggle("--pull", "--no-pull", pull),
        *_flag("--lazy-mode", lazy_mode),
        *_switch("--show-patches", show_patches),
        *_toggle("--pinned", "--no-pinned", pinned),
        *_env_flags(env),
    ]
    raw = self._call(command, timeout=timeout, retry=retry).stdout
    return EnvReport.from_json(raw, base=self._runner.host.source)

exec

exec(argv: Sequence[str], *, names: Iterable[str] = (), groups: Iterable[str] = (), clean: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, pinned: bool | None = None, records_dir: str | Path | None = None, records_name: str | None = None, capture: bool = True, check: bool = True, timeout: MaybeTimeout = UNSET) -> CommandResult

Run a command inside the project's composed environment.

The child's exit code comes back byte for byte, so check=False is how you inspect a failing build instead of catching an exception. Child processes are never retried.

PARAMETER DESCRIPTION
argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

names

Bindings to compose. Omitted composes the default set.

TYPE: Iterable[str] DEFAULT: ()

groups

Groups to compose.

TYPE: Iterable[str] DEFAULT: ()

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

pinned

Resolve through the rendered toolchain's pinned digests rather than following its links — ocx's --pinned / --no-pinned. None leaves the choice to ocx.toml, then OCX_TOOLCHAIN_PINNED, then ocx's default.

TYPE: bool | None DEFAULT: None

records_dir

Write an execution record — the resolved closure and executable, captured just before the child starts — under this directory, which must already exist (ocx warns and skips the record otherwise). None leaves it to [records] dir and OCX_RECORDS_DIR; with none of the three, no record.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template, over {time}, {host}, {pid} and {rand}. None leaves it to [records] name and OCX_RECORDS_NAME.

TYPE: str | None DEFAULT: None

capture

Pipe and capture both streams. False inherits stdio and forwards SIGINT to the child.

TYPE: bool DEFAULT: True

check

Raise on a non-zero child exit instead of returning it.

TYPE: bool DEFAULT: True

timeout

Seconds for the whole run. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The child's exit code and, under capture, its output.

RAISES DESCRIPTION
ValueError

argv is empty — ocx requires a command after --.

OcxProcessError

The child exited non-zero under check.

OcxTimeoutError

The timeout expired.

Source code in src/ocx_sdk/_client.py
def exec(
    self,
    argv: Sequence[str],
    *,
    names: Iterable[str] = (),
    groups: Iterable[str] = (),
    clean: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    pinned: bool | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    capture: bool = True,
    check: bool = True,
    timeout: MaybeTimeout = UNSET,
) -> CommandResult:
    """Run a command inside the project's composed environment.

    The child's exit code comes back byte for byte, so `check=False` is
    how you inspect a failing build instead of catching an exception.
    Child processes are never retried.

    Args:
        argv: The command and its arguments. Must not be empty.
        names: Bindings to compose. Omitted composes the default set.
        groups: Groups to compose.
        clean: Strip the ambient parent environment before composing.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        pinned: Resolve through the rendered toolchain's pinned digests
            rather than following its links — ocx's `--pinned` /
            `--no-pinned`. `None` leaves the choice to `ocx.toml`, then
            `OCX_TOOLCHAIN_PINNED`, then ocx's default.
        records_dir: Write an execution record — the resolved closure
            and executable, captured just before the child starts —
            under this directory, which must already exist (ocx warns
            and skips the record otherwise). `None` leaves it to
            `[records] dir` and `OCX_RECORDS_DIR`; with none of the
            three, no record.
        records_name: The record's filename template, over `{time}`,
            `{host}`, `{pid}` and `{rand}`. `None` leaves it to
            `[records] name` and `OCX_RECORDS_NAME`.
        capture: Pipe and capture both streams. `False` inherits stdio
            and forwards SIGINT to the child.
        check: Raise on a non-zero child exit instead of returning it.
        timeout: Seconds for the whole run. Omitted takes the config's.

    Returns:
        The child's exit code and, under `capture`, its output.

    Raises:
        ValueError: `argv` is empty — ocx requires a command after `--`.
        OcxProcessError: The child exited non-zero under `check`.
        OcxTimeoutError: The timeout expired.
    """
    composed = self._child_argv(argv, names, groups, clean, env, lazy_mode, pinned, records_dir, records_name)
    self._runner.gate()
    return self._runner.finish(composed, capture=capture, check=check, timeout=timeout, retry=None)

exec_async async

exec_async(argv: Sequence[str], *, names: Iterable[str] = (), groups: Iterable[str] = (), clean: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, pinned: bool | None = None, records_dir: str | Path | None = None, records_name: str | None = None, capture: bool = True, check: bool = True, timeout: MaybeTimeout = UNSET) -> CommandResult

Run a command in the project's environment, on the event loop.

PARAMETER DESCRIPTION
argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

names

Bindings to compose.

TYPE: Iterable[str] DEFAULT: ()

groups

Groups to compose.

TYPE: Iterable[str] DEFAULT: ()

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

pinned

Resolve through pinned digests — see exec.

TYPE: bool | None DEFAULT: None

records_dir

Write an execution record here — see exec.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template — see exec.

TYPE: str | None DEFAULT: None

capture

Pipe and capture both streams.

TYPE: bool DEFAULT: True

check

Raise on a non-zero child exit instead of returning it.

TYPE: bool DEFAULT: True

timeout

Seconds for the whole run. Omitted takes the config's.

TYPE: MaybeTimeout DEFAULT: UNSET

RETURNS DESCRIPTION
CommandResult

The child's exit code and, under capture, its output.

RAISES DESCRIPTION
ValueError

argv is empty, or the handle carries an on_log callback, which v0.1 refuses on async paths.

OcxProcessError

The child exited non-zero under check.

OcxTimeoutError

The timeout expired.

Source code in src/ocx_sdk/_client.py
async def exec_async(
    self,
    argv: Sequence[str],
    *,
    names: Iterable[str] = (),
    groups: Iterable[str] = (),
    clean: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    pinned: bool | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    capture: bool = True,
    check: bool = True,
    timeout: MaybeTimeout = UNSET,
) -> CommandResult:
    """Run a command in the project's environment, on the event loop.

    Args:
        argv: The command and its arguments. Must not be empty.
        names: Bindings to compose.
        groups: Groups to compose.
        clean: Strip the ambient parent environment before composing.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        pinned: Resolve through pinned digests — see `exec`.
        records_dir: Write an execution record here — see `exec`.
        records_name: The record's filename template — see `exec`.
        capture: Pipe and capture both streams.
        check: Raise on a non-zero child exit instead of returning it.
        timeout: Seconds for the whole run. Omitted takes the config's.

    Returns:
        The child's exit code and, under `capture`, its output.

    Raises:
        ValueError: `argv` is empty, or the handle carries an `on_log`
            callback, which v0.1 refuses on async paths.
        OcxProcessError: The child exited non-zero under `check`.
        OcxTimeoutError: The timeout expired.
    """
    composed = self._child_argv(argv, names, groups, clean, env, lazy_mode, pinned, records_dir, records_name)
    await self._runner.gate_async()
    return await self._runner.finish_async(composed, capture=capture, check=check, timeout=timeout, retry=None)

spawn

spawn(argv: Sequence[str], *, names: Iterable[str] = (), groups: Iterable[str] = (), clean: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, pinned: bool | None = None, records_dir: str | Path | None = None, records_name: str | None = None, **popen_kw: Any) -> Popen[Any]

Start a command in the project's environment and return Popen.

PARAMETER DESCRIPTION
argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

names

Bindings to compose.

TYPE: Iterable[str] DEFAULT: ()

groups

Groups to compose.

TYPE: Iterable[str] DEFAULT: ()

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call, not the child's environment — that one is SDK-composed.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

pinned

Resolve through pinned digests — see exec.

TYPE: bool | None DEFAULT: None

records_dir

Write an execution record here — see exec.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template — see exec.

TYPE: str | None DEFAULT: None

**popen_kw

Forwarded to Popen. args, shell, and executable are rejected.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Popen[Any]

The running child.

RAISES DESCRIPTION
ValueError

argv is empty, or a rejected keyword was passed.

Source code in src/ocx_sdk/_client.py
def spawn(
    self,
    argv: Sequence[str],
    *,
    names: Iterable[str] = (),
    groups: Iterable[str] = (),
    clean: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    pinned: bool | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    **popen_kw: Any,
) -> subprocess.Popen[Any]:
    """Start a command in the project's environment and return `Popen`.

    Args:
        argv: The command and its arguments. Must not be empty.
        names: Bindings to compose.
        groups: Groups to compose.
        clean: Strip the ambient parent environment before composing.
        env: Extra `[env]` entries for this call, not the child's
            environment — that one is SDK-composed.
        lazy_mode: When content downloads — now, or on first use.
        pinned: Resolve through pinned digests — see `exec`.
        records_dir: Write an execution record here — see `exec`.
        records_name: The record's filename template — see `exec`.
        **popen_kw: Forwarded to `Popen`. `args`, `shell`, and
            `executable` are rejected.

    Returns:
        The running child.

    Raises:
        ValueError: `argv` is empty, or a rejected keyword was passed.
    """
    composed = self._child_argv(argv, names, groups, clean, env, lazy_mode, pinned, records_dir, records_name)
    self._runner.gate()
    return self._runner.launch(composed, popen_kw)

spawn_async async

spawn_async(argv: Sequence[str], *, names: Iterable[str] = (), groups: Iterable[str] = (), clean: bool = False, env: Mapping[str, EnvValue] | None = None, lazy_mode: LazyMode | None = None, pinned: bool | None = None, records_dir: str | Path | None = None, records_name: str | None = None, **popen_kw: Any) -> Process

Start a command in the project's environment, on the event loop.

PARAMETER DESCRIPTION
argv

The command and its arguments. Must not be empty.

TYPE: Sequence[str]

names

Bindings to compose.

TYPE: Iterable[str] DEFAULT: ()

groups

Groups to compose.

TYPE: Iterable[str] DEFAULT: ()

clean

Strip the ambient parent environment before composing.

TYPE: bool DEFAULT: False

env

Extra [env] entries for this call.

TYPE: Mapping[str, EnvValue] | None DEFAULT: None

lazy_mode

When content downloads — now, or on first use.

TYPE: LazyMode | None DEFAULT: None

pinned

Resolve through pinned digests — see exec.

TYPE: bool | None DEFAULT: None

records_dir

Write an execution record here — see exec.

TYPE: str | Path | None DEFAULT: None

records_name

The record's filename template — see exec.

TYPE: str | None DEFAULT: None

**popen_kw

Forwarded to the subprocess factory. args, shell, and executable are rejected.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Process

The running child.

RAISES DESCRIPTION
ValueError

argv is empty, or a rejected keyword was passed.

Source code in src/ocx_sdk/_client.py
async def spawn_async(
    self,
    argv: Sequence[str],
    *,
    names: Iterable[str] = (),
    groups: Iterable[str] = (),
    clean: bool = False,
    env: Mapping[str, EnvValue] | None = None,
    lazy_mode: LazyMode | None = None,
    pinned: bool | None = None,
    records_dir: str | Path | None = None,
    records_name: str | None = None,
    **popen_kw: Any,
) -> asyncio.subprocess.Process:
    """Start a command in the project's environment, on the event loop.

    Args:
        argv: The command and its arguments. Must not be empty.
        names: Bindings to compose.
        groups: Groups to compose.
        clean: Strip the ambient parent environment before composing.
        env: Extra `[env]` entries for this call.
        lazy_mode: When content downloads — now, or on first use.
        pinned: Resolve through pinned digests — see `exec`.
        records_dir: Write an execution record here — see `exec`.
        records_name: The record's filename template — see `exec`.
        **popen_kw: Forwarded to the subprocess factory. `args`,
            `shell`, and `executable` are rejected.

    Returns:
        The running child.

    Raises:
        ValueError: `argv` is empty, or a rejected keyword was passed.
    """
    composed = self._child_argv(argv, names, groups, clean, env, lazy_mode, pinned, records_dir, records_name)
    await self._runner.gate_async()
    return await self._runner.launch_async(composed, popen_kw)

ConfigOverrides

Bases: TypedDict

The OcxConfig fields a with_config call may replace, all optional.

What makes Ocx.with_config(...) and Project.with_config(...) check their keyword arguments instead of accepting **overrides: Any, so a misspelled field is a type error rather than a TypeError at runtime. Public for the same reason MaybeRetry is: a wrapper around this SDK can forward a caller's overrides with **overrides: Unpack[ConfigOverrides] rather than re-declaring the field list itself.

Mirrors OcxConfig field for field — a field added there and not here is a field with_config would refuse.

Optional at the class level rather than per field: this module postpones annotation evaluation, and a NotRequired[...] inside a string annotation is invisible to __required_keys__, so anything introspecting the type at runtime would be told every field was mandatory. Every field is optional here anyway, which is exactly what total=False says.

Example
from typing import Unpack

from ocx_sdk import ConfigOverrides, Ocx


def hermetic(ocx: Ocx, **overrides: Unpack[ConfigOverrides]) -> Ocx:
    return ocx.with_config(no_config=True, **overrides)

OcxConfig dataclass

OcxConfig(home: Path | None = None, offline: bool = False, frozen: bool = False, config: Path | None = None, no_config: bool = False, managed_config: str | None = None, auth: Mapping[str, Auth] = dict[str, Auth](), insecure_registries: Collection[str] | None = None, sigstore_trusted_root: str | Path | None = None, docker_config: Path | None = None, index: Path | None = None, jobs: int | None = None, log_level: LogLevel | None = None, mirrors: Mapping[str, str] | None = None, no_update_check: bool = True, no_config_refresh: bool | None = None, consent: bool = False, records_dir: Path | None = None, records_name: str | None = None, toolchain_dir: Path | None = None, forge_token: str | None = None, forge_git_token: str | None = None, forge_git_username: str | None = None, retry: RetryPolicy | None = None, timeout: float | None = None)

Policy for every ocx process a handle spawns.

Containers are snapshotted at construction — the mappings as read-only proxies, insecure_registries as a tuple — so neither mutating the argument afterwards nor reaching into the field can rewrite a config a handle already holds. Derive a variant with Ocx.with_config(...) rather than reaching for a mutation that the frozen dataclass refuses.

ATTRIBUTE DESCRIPTION
home

$OCX_HOME for the spawned process; None keeps ocx's default.

TYPE: Path | None

offline

Refuse every network access.

TYPE: bool

frozen

Refuse any lockfile change.

TYPE: bool

config

Explicit config.toml; None keeps ocx's discovery chain.

TYPE: Path | None

no_config

Hermetic mode — drops the discovered chain and the managed tier. Wins over managed_config when both are set, which is ocx's own precedence, not an SDK invention.

TYPE: bool

managed_config

OCI reference of the managed config tier. MANAGED_CONFIG_DISABLED force-disables an ambient one; None leaves the ambient setting alone.

TYPE: str | None

auth

Credentials per registry, keyed lowercase. An entry wins over an ambient OCX_AUTH_<SLUG>_* for the same registry, whatever case that variable spells the slug in.

TYPE: Mapping[str, Auth]

insecure_registries

Registries reachable over plaintext HTTP. None inherits the ambient set; any explicit value replaces it entirely, so () blocks an ambient re-enable (fail-closed).

TYPE: Collection[str] | None

sigstore_trusted_root

The Sigstore trusted root to verify signatures against, for air-gapped verification against a private root of trust. None is not "leave the ambient value" here, unlike the other path-shaped fields: it means ocx's own default root, and _env pops an ambient OCX_SIGSTORE_TRUSTED_ROOT to enforce that. The variable redefines what "trusted" means and outranks the config file, so an ambient one could otherwise repoint Fulcio, CT, and Rekor at an attacker's material while every install still reported verified. The path travels verbatim, as home, config, index and docker_config do: ~ is not expanded, and a relative path resolves against the child's working directory, not the caller's. That is acceptable rather than sloppy, because a path ocx cannot read is not a path it falls back from — an explicit override is read with ?, so an unreadable one aborts the command at exit 74 (IoError) instead of quietly verifying against the default root. A wrong value fails loudly; it never becomes a silent trust substitution, which is the hazard this field exists to close. Pass an absolute path when the call must not depend on where the child starts.

TYPE: str | Path | None

docker_config

Directory for DOCKER_CONFIG. Keep it 0700 — it holds registry credentials.

TYPE: Path | None

index

Explicit index path; None keeps ocx's default.

TYPE: Path | None

jobs

Parallel job cap; None keeps ocx's default.

TYPE: int | None

log_level

ocx trace verbosity. At 'trace' ocx may print secrets of its own; the SDK can only redact the values it was given.

TYPE: LogLevel | None

mirrors

Registry host to mirror host, serialized into OCX_MIRRORS.

TYPE: Mapping[str, str] | None

no_update_check

Suppress ocx's update check. True by default — an SDK call is a program step, not an interactive session.

TYPE: bool

no_config_refresh

Suppress the managed-config refresh; None leaves the decision to whoever owns that tier.

TYPE: bool | None

consent

Let the project-tier mutators (add, lock, pull, exec, update, init) record the per-project consent stamp that lets a shell prompt in that directory activate the project. False by default — an SDK call is a program step, and a stamp it left behind would make the project live at the developer's next prompt without anyone having run ocx shell allow. Travels as OCX_NO_CONSENT, written either way so an ambient value cannot decide it. This is the per-project stamp, not the shell-activation consent ocx shell allow records — that command is not wrapped.

TYPE: bool

records_dir

Where exec and package exec write execution records — OCX_RECORDS_DIR. None leaves it to [records] dir in the project (or an ambient value); without any, no record is written. The per-call records_dir= on the exec verbs outranks this.

TYPE: Path | None

records_name

The record filename template — OCX_RECORDS_NAME. None leaves it to [records] name or ocx's default.

TYPE: str | None

toolchain_dir

The root ocx renders project toolchains under — OCX_TOOLCHAIN_DIR — instead of <project>/.ocx/toolchain/. None keeps ocx's default. OCX_TOOLCHAIN_PINNED and OCX_TOOLCHAIN_ACTIVATE are deliberately not modelled: the first is the weakest tier of a choice pinned= makes at the call site, the second governs shell activation the SDK never performs.

TYPE: Path | None

forge_token

The forge API credential package.announce and package.claim open their request with — OCX_ANNOUNCE_TOKEN. None leaves an ambient one alone; explicit wins over ambient, as auth does. Redacted from every log and error surface.

TYPE: str | None

forge_git_token

The push secret for transport="git" — OCX_ANNOUNCE_GIT_TOKEN. Falls back to forge_token upstream when unset. Redacted likewise.

TYPE: str | None

forge_git_username

The username the git transport pushes as — OCX_ANNOUNCE_GIT_USERNAME. Not a secret.

TYPE: str | None

retry

Retry policy for failures ocx marked transient; None disables retrying.

TYPE: RetryPolicy | None

timeout

Per-attempt budget in seconds; None waits indefinitely.

TYPE: float | None

Example

OcxConfig().insecure_registries is None True OcxConfig(insecure_registries=()).insecure_registries ()

ComposedEnv dataclass

ComposedEnv(_values: Mapping[str, str])

A merged environment, ready for a child process.

Build one with merge(); EnvReport.compose() is the caller-facing route.

The values stay off every human-readable surface: a composed environment inherits whatever OCX_AUTH_* its base carried, and a repr is exactly what a traceback or a pytest diff prints (CWE-532). __repr__ names the keys and masks every value, the same shape SpawnEnv uses.

mapping property

mapping: dict[str, str]

Return the merged environment as a fresh dict.

The non-invasive form, and the documented default: pass it as subprocess.run(..., env=...) instead of mutating the process.

activate

activate() -> Generator[None]

Apply this environment to os.environ for the duration of the block.

Process-global and single-owner: it changes what every thread and every subprocess sees, and a second, overlapping activate() raises instead of nesting. Concurrent code passes mapping to the subprocess call.

The revert is diff-based — only the keys this environment sets are recorded on entry and restored on exit, keys that were absent are deleted again, and unrelated changes made inside the block survive. It runs in finally; os._exit, execve, and SIGKILL leave no chance to run it at all.

RAISES DESCRIPTION
RuntimeError

Another activate() block already owns os.environ.

Source code in src/ocx_sdk/_envmodel.py
@contextmanager
def activate(self) -> Generator[None]:
    """Apply this environment to `os.environ` for the duration of the block.

    Process-global and single-owner: it changes what every thread and every
    subprocess sees, and a second, overlapping `activate()` raises instead
    of nesting. Concurrent code passes `mapping` to the subprocess call.

    The revert is diff-based — only the keys this environment sets are
    recorded on entry and restored on exit, keys that were absent are
    deleted again, and unrelated changes made inside the block survive.
    It runs in `finally`; `os._exit`, `execve`, and SIGKILL leave no chance
    to run it at all.

    Raises:
        RuntimeError: Another `activate()` block already owns `os.environ`.
    """
    global _active
    # Ownership spans the entry snapshot, the body, and the restore. The
    # lock itself covers only the check-and-set and the release, never any
    # of that: holding it across the body would serialize callers and fake
    # a nesting safety the process-global contract deliberately does not
    # offer.
    with _ownership:
        if _active is not None:
            raise RuntimeError(
                "another ComposedEnv.activate() block already owns os.environ; activate() is "
                "process-global and does not nest — pass `.mapping` to the subprocess call instead."
            )
        _active = self
    # Read before writing: the prior state of exactly the keys about to
    # change, with `None` standing for "was not set" (an env value never is).
    restore = {key: os.environ.get(key) for key in self._values}
    try:
        os.environ.update(self._values)
        yield
    finally:
        # Ownership is released only once the environment is back the way
        # it was found. Clearing it first would let a waiting thread's
        # activate() succeed mid-revert, and this restore would then
        # overwrite the values that thread had just applied.
        try:
            for key, previous in restore.items():
                if previous is None:
                    os.environ.pop(key, None)
                else:
                    os.environ[key] = previous
        finally:
            with _ownership:
                _active = None

AuthError

AuthError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

Registry authentication or authorization failed (exit 80).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

BootstrapError

Bases: OcxError

Base for failures while resolving, downloading, or installing an ocx binary.

ChecksumMismatchError

Bases: BootstrapError

A downloaded artifact did not match the sha256 the manifest declared.

ConfigError

ConfigError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

ocx's configuration is missing or invalid (exit 78).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

DataError

DataError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

Input or resolved data was malformed, ambiguous, or platform-mismatched (exit 65).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

DirtyRcBlockError

DirtyRcBlockError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

A managed-config fence carried local edits and refused to overwrite them (exit 82).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

DistManifestError

Bases: BootstrapError

A dist manifest was malformed, unparseable, or failed field validation.

DownloadError

Bases: BootstrapError

A manifest or artifact could not be fetched.

ExitCode

Bases: IntEnum

Exit codes ocx assigns to error categories (sysexits.h plus ocx's own).

TEMP_FAIL is the retry signal — ocx maps 429/5xx/network timeouts onto it, while UNAVAILABLE marks a failure that retrying cannot fix.

ForgeCapabilityUnavailableError

ForgeCapabilityUnavailableError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

A forge was reached and refused a write for want of a capability (exit 86).

Raised by announce/claim under --transport git when job-token push is disabled on the index project or the publishing project is missing from its allowlist. Distinct from 69 (the forge could not be reached at all) and from 80 (the credential is valid here — the refusal is not on it).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

IoError

IoError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

A filesystem or network I/O operation failed (exit 74).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

NotFoundError

NotFoundError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

A package, registry, or reference does not exist (exit 79).

Covers both things ocx can fail to find: the identifier a call named, and the project file a --project pointed at.

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

OcxError

Bases: Exception

Base class for every error raised by ocx-sdk.

Subclasses set _hint: the next step a caller can actually take. It is appended to the message of every error in the hierarchy.

Errors cross process boundaries intact — a ProcessPoolExecutor worker or a pytest-xdist node has to be able to hand one back.

OcxExecutionError

OcxExecutionError(argv: Sequence[str], stderr: str = '')

Bases: OcxError

Base for failures an ocx process raised — non-zero exits and timeouts.

except OcxExecutionError is the catch shape a caller usually wants: it covers both a process that failed and one that never finished.

ATTRIBUTE DESCRIPTION
argv

The argv that ran, with secrets already redacted by _process.

TYPE: tuple[str, ...]

stderr

Captured stderr in full; the message shows only its tail.

TYPE: str

Source code in src/ocx_sdk/_errors.py
def __init__(self, argv: Sequence[str], stderr: str = "") -> None:
    super().__init__()
    self.argv = tuple(argv)
    self.stderr = stderr

OcxNotFoundError

Bases: OcxError

No ocx binary could be discovered.

OcxProcessError

OcxProcessError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxExecutionError

An ocx process exited non-zero.

ATTRIBUTE DESCRIPTION
exit_code

The exit status. int rather than ExitCode because a process killed by a signal exits with a code ocx never assigns, and building an error must not itself raise.

TYPE: int

attempts

Attempts made; greater than 1 when a retry policy was active. _retry sets it on the final failure.

TYPE: int

stdout

Captured stdout in full, verbatim. Never read by _summary() or __str__ — that is the entire point: a partial-failure report on stdout can carry data that must not reach a log through the exception message. Never redacted either, unlike stderr — substituting inside it would corrupt the JSON a caller is about to parse. Read it through partial_report(err), which is the sanctioned consumer; never hand the raw error or this attribute to a log sink, to a logger that serializes __dict__, or to a pickle crossing into a lower-trust process.

TYPE: str

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

retryable property

retryable: bool

Whether ocx called this failure transient (exit 75).

Fixed semantics, independent of any RetryPolicy: this reports what ocx said about the failure, not whether a caller chose to retry it.

OcxTimeoutError

OcxTimeoutError(timeout: float, argv: Sequence[str], stderr: str = '')

Bases: OcxExecutionError

ocx did not finish within the timeout and the child was terminated.

ATTRIBUTE DESCRIPTION
timeout

The per-attempt budget, in seconds, that expired.

TYPE: float

argv

The argv that ran.

TYPE: tuple[str, ...]

stderr

Whatever stderr was captured before the child was killed.

TYPE: str

Source code in src/ocx_sdk/_errors.py
def __init__(self, timeout: float, argv: Sequence[str], stderr: str = "") -> None:
    super().__init__(argv, stderr)
    self.timeout = timeout

PermissionDeniedError

PermissionDeniedError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

ocx was denied access to a resource (exit 77).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

PolicyBlockedError

PolicyBlockedError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

ocx refused the operation under an offline or frozen policy (exit 81).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

ReferrersUnsupportedError

ReferrersUnsupportedError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

The registry served neither the OCI 1.1 Referrers API nor the Referrers Tag Schema fallback (exit 84).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

TempFailError

TempFailError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

A transient failure ocx marked as retry-safe (exit 75).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

TransparencyLogUnavailableError

TransparencyLogUnavailableError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

Rekor was unreachable during a signing or verification operation (exit 83).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

UnavailableError

UnavailableError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

A resource is unavailable and ocx classified it as non-transient (exit 69).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

UnsupportedKeyBackendError

UnsupportedKeyBackendError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

ocx recognized the key backend scheme but has not implemented it (exit 85).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

UnsupportedPlatformError

Bases: BootstrapError

No ocx release matches the host platform.

UsageError

UsageError(exit_code: int, argv: Sequence[str], stderr: str = '', *, attempts: int = 1, stdout: str = '')

Bases: OcxProcessError

ocx rejected the command line (exit 64).

Source code in src/ocx_sdk/_errors.py
def __init__(
    self,
    exit_code: int,
    argv: Sequence[str],
    stderr: str = "",
    *,
    attempts: int = 1,
    stdout: str = "",
) -> None:
    super().__init__(argv, stderr)
    self.exit_code = exit_code
    self.attempts = attempts
    self.stdout = stdout

VersionCompatError

VersionCompatError(found: str, minimum: str)

Bases: OcxError

The discovered ocx binary is older than this SDK supports.

ATTRIBUTE DESCRIPTION
found

The version the binary reported.

TYPE: str

minimum

The oldest version this SDK is tested against.

TYPE: str

Source code in src/ocx_sdk/_errors.py
def __init__(self, found: str, minimum: str) -> None:
    super().__init__(f"ocx {found} is older than the minimum supported {minimum}.")
    self.found = found
    self.minimum = minimum

AboutInfo dataclass

AboutInfo(version: str, registry: str, home: str, shell: str | None, platforms: tuple[str, ...] = (), libc: tuple[str, ...] = (), channel: str | None = None, commit: Mapping[str, Any] = _EMPTY, build: Mapping[str, Any] = _EMPTY, ci: Mapping[str, Any] = _EMPTY)

ocx about — the binary plus the host it is running on.

channel appears only when it was baked into the build. shell is always on the wire and may be null (about.rs:30) — ocx detected no shell — so it is the only one of the four leading fields that can be None.

from_json classmethod

from_json(raw: str) -> AboutInfo

Parse ocx --format json about output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> AboutInfo:
    """Parse `ocx --format json about` output."""
    data = _object(raw, "about")
    return cls(
        version=_need(data, "version", "about"),
        registry=_need(data, "registry", "about"),
        home=_need(data, "home", "about"),
        shell=_need(data, "shell", "about"),
        platforms=_texts(_need(data, "platforms", "about")),
        libc=_texts(_need(data, "libc", "about")),
        channel=data.get("channel"),
        commit=_table(data.get("commit")),
        build=_table(data.get("build")),
        ci=_table(data.get("ci")),
    )

Advisory dataclass

Advisory(kind: str, package: str, message: str, key: str | None = None)

Something ocx wants the caller to know about a composed environment.

ATTRIBUTE DESCRIPTION
kind

The machine discriminator to branch on, never message.

TYPE: str

package

The package the advisory is about.

TYPE: str

message

The human rendering of the same fact.

TYPE: str

key

The environment variable named, or None for an advisory that names none. Absent-when-unused (D7): env.rs:175 skips the key, and the undeclared-binaries variant (env.rs:189) emits no key at all, so reading it as required crashes env() outright.

TYPE: str | None

ref property

The package the advisory is about, as a PackageRef.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> Advisory

Build from one decoded advisories entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Advisory:
    """Build from one decoded `advisories` entry."""
    return cls(
        kind=_need(data, "kind", "env"),
        package=_need(data, "package", "env"),
        message=_need(data, "message", "env"),
        key=data.get("key"),
    )

AnnounceReport dataclass

AnnounceReport(package: str, status: str, desc_status: str, forge: str, transport: str, credential_kind: str, push_credential_kind: str | None, branch: str | None, pull_request_url: str | None, pull_request_number: int | None, fork: str | None, written_paths: tuple[str, ...], capability_checks: tuple[CapabilityCheck, ...], reserved_tags_dropped: tuple[str, ...])

ocx package announce — what reached the index (C-061).

Payload is bare (D11). Every one of the fourteen keys is always on the wire; the nullable ones are null rather than absent, so all are read with _need and typed | None where upstream is Option.

ATTRIBUTE DESCRIPTION
package

The announced <namespace>/<package>.

TYPE: str

status

"updated" when a rebuilt root was committed, "unchanged" when it was byte-identical to the committed one. An unchanged run can still report a pull request it ensured.

TYPE: str

desc_status

"updated" when the package's __ocx.desc artifact moved, else "unchanged".

TYPE: str

forge

"github" or "gitlab", as resolved.

TYPE: str

transport

"api" or "git".

TYPE: str

credential_kind

"job-token", "token" or "none" — what the identity ladder resolved for the API credential.

TYPE: str

push_credential_kind

"job-token", "token", "git-helper", or None under the api transport, which pushes nothing.

TYPE: str | None

branch

The announce branch, or None under --out.

TYPE: str | None

pull_request_url

The request's URL, when one was opened or ensured.

TYPE: str | None

pull_request_number

Its number, likewise.

TYPE: int | None

fork

The fork the request came from, when --fork was given.

TYPE: str | None

written_paths

Files written under --out; empty otherwise.

TYPE: tuple[str, ...]

capability_checks

The forge probes, in CapabilityName order.

TYPE: tuple[CapabilityCheck, ...]

reserved_tags_dropped

__ocx and legacy sha256.<hex> tags named on the command line and dropped: a reported fact of a successful run, never a failure.

TYPE: tuple[str, ...]

from_json classmethod

from_json(raw: str) -> AnnounceReport

Parse ocx --format json package announce output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> AnnounceReport:
    """Parse `ocx --format json package announce` output."""
    data = _object(raw, _ANNOUNCE)
    return cls(
        package=_need(data, "package", _ANNOUNCE),
        status=_need(data, "status", _ANNOUNCE),
        desc_status=_need(data, "desc_status", _ANNOUNCE),
        forge=_need(data, "forge", _ANNOUNCE),
        transport=_need(data, "transport", _ANNOUNCE),
        credential_kind=_need(data, "credential_kind", _ANNOUNCE),
        push_credential_kind=_need(data, "push_credential_kind", _ANNOUNCE),
        branch=_need(data, "branch", _ANNOUNCE),
        pull_request_url=_need(data, "pull_request_url", _ANNOUNCE),
        pull_request_number=_need(data, "pull_request_number", _ANNOUNCE),
        fork=_need(data, "fork", _ANNOUNCE),
        written_paths=_texts(_need(data, "written_paths", _ANNOUNCE)),
        capability_checks=tuple(
            CapabilityCheck.from_dict(row) for row in _rows(_need(data, "capability_checks", _ANNOUNCE))
        ),
        reserved_tags_dropped=_texts(_need(data, "reserved_tags_dropped", _ANNOUNCE)),
    )

Assertion dataclass

Assertion(kind: str, message: str)

Why package test decided a run failed.

ATTRIBUTE DESCRIPTION
kind

The assertion that fired, e.g. "exit_code".

TYPE: str

message

Its human-readable form.

TYPE: str

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> Assertion

Build from the decoded assertion object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Assertion:
    """Build from the decoded `assertion` object."""
    return cls(kind=_need(data, "kind", "package test"), message=_need(data, "message", "package test"))

AttestationOutcome dataclass

AttestationOutcome(status: str, referrer_digest: str | None = None, sidecar_digest: str | None = None, predicate_type: str | None = None, signed: bool | None = None, kind: str | None = None, message: str | None = None)

The attestation outcome of one push(..., sign=True) call (C-019).

push.rs:161, internally tagged on status. A bare str (the usual D8 treatment for a scalar enum) would silently drop predicate_type and signed, so this carries every field instead, all but status optional: succeeded populates predicate_type/signed and at least one of referrer_digest/sidecar_digest; failed populates kind/message.

ATTRIBUTE DESCRIPTION
status

"succeeded" or "failed".

TYPE: str

referrer_digest

The attestation's referrer-API digest, on success.

TYPE: str | None

sidecar_digest

The attestation's sidecar-tag digest, on success. --signature-format both populates both digest fields.

TYPE: str | None

predicate_type

The resolved predicate type URI, on success.

TYPE: str | None

signed

Whether the attestation was itself signed, on success.

TYPE: bool | None

kind

The machine-branchable failure slug, on failure.

TYPE: str | None

message

Sanitized prose describing the failure, on failure.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> AttestationOutcome

Build from the decoded PushResult.attestation object.

status selects the variant, and each variant's own fields carry no skip_serializing_if — so they are read with _need under the status that emits them. signed matters most: reading it with .get failed open, turning "nothing vouches for this document" into None.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> AttestationOutcome:
    """Build from the decoded `PushResult.attestation` object.

    `status` selects the variant, and each variant's own fields carry no
    `skip_serializing_if` — so they are read with `_need` under the status
    that emits them. `signed` matters most: reading it with `.get` failed
    open, turning "nothing vouches for this document" into `None`.
    """
    status = _need(data, "status", _PUSH)
    if status not in (_ATTESTATION_SUCCEEDED, _ATTESTATION_FAILED):
        raise ValueError(
            f"`ocx {_PUSH}` reported attestation status {status!r}; this SDK knows "
            f"{_ATTESTATION_SUCCEEDED!r} and {_ATTESTATION_FAILED!r}. Refusing rather than "
            "reading it as a failure — a third variant carries fields this SDK would drop."
        )
    succeeded = status == _ATTESTATION_SUCCEEDED
    return cls(
        status=status,
        referrer_digest=data.get("referrer_digest"),
        sidecar_digest=data.get("sidecar_digest"),
        predicate_type=_need(data, "predicate_type", _PUSH) if succeeded else None,
        signed=_need(data, "signed", _PUSH) if succeeded else None,
        kind=None if succeeded else _need(data, "kind", _PUSH),
        message=None if succeeded else _need(data, "message", _PUSH),
    )

AttestationReport dataclass

AttestationReport(identifier: str, platform: str, subject_digest: str, predicate_type: str, signed: bool, transparency_log_index: int | None, bundle_digest: str | None = None, referrer_digest: str | None = None, sidecar_digest: str | None = None, certificate_identity: str | None = None, certificate_oidc_issuer: str | None = None, key_backend: str | None = None, public_key_hint: str | None = None)

ocx package attest on a single reference — no tag sweep (C-013).

Payload is enveloped (D11): from_json unwraps data. Flat, thirteen fields; per D7's field-order rule the seven optionals move to the end even though upstream interleaves public_key_hint earlier.

ATTRIBUTE DESCRIPTION
identifier

The identifier that was attested.

TYPE: str

platform

The attested platform.

TYPE: str

subject_digest

The manifest digest the attestation covers.

TYPE: str

predicate_type

The resolved predicate type URI — not the --type spelling the caller gave attest(predicate_type=...).

TYPE: str

signed

Whether the attestation itself was signed.

TYPE: bool

transparency_log_index

The Rekor entry index. Always present in the JSON, but None when nothing was logged.

TYPE: int | None

bundle_digest

Digest of the attestation bundle, when produced.

TYPE: str | None

referrer_digest

Digest under the OCI referrers API, when attached that way.

TYPE: str | None

sidecar_digest

Digest of the sidecar tag, when attached that way. --signature-format both populates both digest fields.

TYPE: str | None

certificate_identity

The Fulcio certificate's identity, for keyless signing.

TYPE: str | None

certificate_oidc_issuer

The Fulcio certificate's OIDC issuer, for keyless signing.

TYPE: str | None

key_backend

Which key backend produced the signature, carried as raw str (D8).

TYPE: str | None

public_key_hint

A hint identifying the verifying key, when supplied.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> AttestationReport

Build from a decoded attest report.

Used both for the envelope's data (a bare attest) and, through C-017's row-parser seam, for one SweptTagReport.report row of a swept attest.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> AttestationReport:
    """Build from a decoded attest report.

    Used both for the envelope's `data` (a bare `attest`) and, through
    C-017's row-parser seam, for one `SweptTagReport.report` row of a
    swept `attest`.
    """
    return cls(
        identifier=_need(data, "identifier", _ATTEST),
        platform=_need(data, "platform", _ATTEST),
        subject_digest=_need(data, "subject_digest", _ATTEST),
        predicate_type=_need(data, "predicate_type", _ATTEST),
        signed=_need(data, "signed", _ATTEST),
        transparency_log_index=_need(data, "transparency_log_index", _ATTEST),
        bundle_digest=data.get("bundle_digest"),
        referrer_digest=data.get("referrer_digest"),
        sidecar_digest=data.get("sidecar_digest"),
        certificate_identity=data.get("certificate_identity"),
        certificate_oidc_issuer=data.get("certificate_oidc_issuer"),
        key_backend=data.get("key_backend"),
        public_key_hint=data.get("public_key_hint"),
    )

from_json classmethod

from_json(raw: str) -> AttestationReport

Parse ocx --format json package attest output (no tag sweep).

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> AttestationReport:
    """Parse `ocx --format json package attest` output (no tag sweep)."""
    return cls.from_dict(_envelope(raw, _ATTEST))

BlobSummary dataclass

BlobSummary(present: int, mounted: int, uploaded: int)

Blob transfer counts for one copy (C-015, package_copy.rs:140-147).

Not push's layers — that is a different struct (LayerCounts, oci/client.rs:75-84) with a different field set, {mounted, uploaded, verified}. Copy tracks transfer method including blobs that needed no transfer at all; push tracks mount/upload/verify- by-digest. Reusing push's shape here would look for a verified key that copy never sends and silently drop present — the D7 failure class this module exists to avoid.

ATTRIBUTE DESCRIPTION
present

Blobs already present at the target — no transfer needed.

TYPE: int

mounted

Blobs mounted from another repository without a re-upload.

TYPE: int

uploaded

Blobs actually transferred.

TYPE: int

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> BlobSummary

Build from the decoded blobs object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> BlobSummary:
    """Build from the decoded `blobs` object."""
    return cls(
        present=_need(data, "present", _COPY),
        mounted=_need(data, "mounted", _COPY),
        uploaded=_need(data, "uploaded", _COPY),
    )

BuildReceipt dataclass

BuildReceipt(platform: str | None = None, identifier: str | None = None)

The <bundle-stem>-receipt.json sidecar ocx package create writes.

ocx package receipt <bundle> prints what create was told — the platform it resolved against and the identifier the bundle will publish under — so push and test need not restate either. Both keys are absent when the build recorded neither, never null, so both are read with .get.

The receipt file itself (<stem>-receipt.json, build_receipt.rs) is ocx's; this SDK never reads it directly. Its format version stays behind the command, which answers exit 65 for a file it cannot read and exit 79 for a bundle with no receipt beside it.

ATTRIBUTE DESCRIPTION
platform

The --platform recorded, canonical grammar, when given.

TYPE: str | None

identifier

The --identifier recorded, resolved against the default registry, when given.

TYPE: str | None

ref property

ref: PackageRef | None

The recorded identifier as a PackageRef, or None when none was recorded.

from_json classmethod

from_json(raw: str) -> BuildReceipt

Parse ocx --format json package receipt output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> BuildReceipt:
    """Parse `ocx --format json package receipt` output."""
    return cls.from_dict(_object(raw, _RECEIPT))

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> BuildReceipt

Build from the decoded report.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> BuildReceipt:
    """Build from the decoded report."""
    return cls(platform=data.get("platform"), identifier=data.get("identifier"))

Candidate dataclass

Candidate(digest: str, pinned: str, platform: str, media_type: str | None = None, size: int | None = None)

One platform's manifest for a package, as inspect lists it.

media_type and size are present on the package inspect tier only; the toolchain tier omits them.

ref property

The pinned identifier, ready for the next call's argv.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> Candidate

Build from one decoded entry of a package's candidates array.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Candidate:
    """Build from one decoded entry of a package's `candidates` array."""
    return cls(
        digest=_need(data, "digest", "inspect"),
        pinned=_need(data, "pinned", "inspect"),
        platform=_need(data, "platform", "inspect"),
        media_type=data.get("media_type"),
        size=data.get("size"),
    )

CapabilityCheck dataclass

CapabilityCheck(name: str, status: str, detail: str | None)

One forge capability announce/claim probed before writing.

ATTRIBUTE DESCRIPTION
name

The capability — git-version, push-access, job-token-push or job-token-allowlist — carried as raw str (D8).

TYPE: str

status

What the probe found: ok, skipped for a check the transport made inapplicable, or a failure status. Raw str.

TYPE: str

detail

What the probe saw, when it recorded anything. Always on the wire, null when empty.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> CapabilityCheck

Build from one decoded capability_checks entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> CapabilityCheck:
    """Build from one decoded `capability_checks` entry."""
    return cls(
        name=_need(data, "name", _ANNOUNCE),
        status=_need(data, "status", _ANNOUNCE),
        detail=_need(data, "detail", _ANNOUNCE),
    )

CascadeCheckReport dataclass

CascadeCheckReport(reports: tuple[CascadeReport, ...], exit_code: int = 0)

ocx package cascade check — one audit per package, in input order.

Report-then-fail: a finding makes the process exit 65 with this document on stdout, so the client tolerates that code and hands the report back as a result (tolerated_report). exit_code is the process's own, carried so a caller can branch without re-deriving it from the rows.

ATTRIBUTE DESCRIPTION
reports

One entry per package audited.

TYPE: tuple[CascadeReport, ...]

exit_code

0 when everything agreed, 65 when anything did not.

TYPE: int

clean property

clean: bool

Whether the audit found nothing — the exit code's answer.

from_json classmethod

from_json(raw: str, *, exit_code: int = 0) -> CascadeCheckReport

Parse ocx --format json package cascade check output.

PARAMETER DESCRIPTION
raw

Captured stdout.

TYPE: str

exit_code

The process's exit status, carried onto the result.

TYPE: int DEFAULT: 0

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str, *, exit_code: int = 0) -> CascadeCheckReport:
    """Parse `ocx --format json package cascade check` output.

    Args:
        raw: Captured stdout.
        exit_code: The process's exit status, carried onto the result.
    """
    data = _object(raw, _CASCADE_CHECK)
    reports = _rows(_need(data, "reports", _CASCADE_CHECK))
    return cls(reports=tuple(CascadeReport.from_dict(row) for row in reports), exit_code=exit_code)

CascadeRepairReport dataclass

CascadeRepairReport(entries: tuple[RepairEntry, ...], dry_run: bool, announce_tags_path: str | None, exit_code: int = 0)

ocx package cascade repair — what was re-pointed, or would be.

Report-then-fail like check: exit 65 with this document when a finding remains — including on a dry_run, where "planned anything" is the finding — so exit_code rather than an exception says whether the registry now agrees with itself.

ATTRIBUTE DESCRIPTION
entries

One entry per package, in input order.

TYPE: tuple[RepairEntry, ...]

dry_run

True when nothing was written because the run was a preview.

TYPE: bool

announce_tags_path

Where --announce-tags was written, when given.

TYPE: str | None

exit_code

0 when every attempted write succeeded and nothing remains, 65 otherwise.

TYPE: int

clean property

clean: bool

Whether no finding remains — the exit code's answer.

from_json classmethod

from_json(raw: str, *, exit_code: int = 0) -> CascadeRepairReport

Parse ocx --format json package cascade repair output.

PARAMETER DESCRIPTION
raw

Captured stdout.

TYPE: str

exit_code

The process's exit status, carried onto the result.

TYPE: int DEFAULT: 0

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str, *, exit_code: int = 0) -> CascadeRepairReport:
    """Parse `ocx --format json package cascade repair` output.

    Args:
        raw: Captured stdout.
        exit_code: The process's exit status, carried onto the result.
    """
    data = _object(raw, _CASCADE_REPAIR)
    return cls(
        entries=tuple(RepairEntry.from_dict(row) for row in _rows(_need(data, "entries", _CASCADE_REPAIR))),
        dry_run=_need(data, "dry_run", _CASCADE_REPAIR),
        announce_tags_path=_need(data, "announce_tags_path", _CASCADE_REPAIR),
        exit_code=exit_code,
    )

CascadeReport dataclass

CascadeReport(identifier: str, logical: str | None, aliases: Mapping[str, Mapping[str, Any]], rows: tuple[SlotRow, ...], index_findings: tuple[IndexFinding, ...], ignored_tags: tuple[str, ...], unrepairable: tuple[Mapping[str, Any], ...])

One package's rolling-tag audit — the check document, and the report inside a repair entry.

First-cut depth: the rows, the index findings and the tag lists are typed; aliases and unrepairable are carried as opaque mappings (Integration.payload's precedent) until a consumer needs their fields.

ATTRIBUTE DESCRIPTION
identifier

The physical repository the graph was read from.

TYPE: str

logical

The logical name the caller gave, when it differed.

TYPE: str | None

aliases

Rolling tag to its alias state ({"state": "present"}, {"state": "absent"}, or {"state": "not-an-index", "digest": ...}), untyped.

TYPE: Mapping[str, Mapping[str, Any]]

rows

One slot per (rolling tag, platform).

TYPE: tuple[SlotRow, ...]

index_findings

Public-index disagreements; empty off ocx.sh.

TYPE: tuple[IndexFinding, ...]

ignored_tags

Tags the audit skipped — the keep tags among them.

TYPE: tuple[str, ...]

unrepairable

Findings repair refuses to fix, each {tag, reason, digest?}, untyped.

TYPE: tuple[Mapping[str, Any], ...]

ref property

The audited repository, as a PackageRef.

clean property

clean: bool

Whether nothing disagrees: every row ok, no index finding, nothing unrepairable.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> CascadeReport

Build from one decoded reports entry (or a repair entry's report).

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> CascadeReport:
    """Build from one decoded `reports` entry (or a repair entry's `report`)."""
    aliases = {tag: _table(state) for tag, state in _table(_need(data, "aliases", _CASCADE)).items()}
    return cls(
        identifier=_need(data, "identifier", _CASCADE),
        logical=_need(data, "logical", _CASCADE),
        aliases=MappingProxyType(aliases),
        rows=tuple(SlotRow.from_dict(row) for row in _rows(_need(data, "rows", _CASCADE))),
        index_findings=tuple(IndexFinding.from_dict(row) for row in _rows(_need(data, "index_findings", _CASCADE))),
        ignored_tags=_texts(_need(data, "ignored_tags", _CASCADE)),
        unrepairable=_rows(_need(data, "unrepairable", _CASCADE)),
    )

ClaimOwner dataclass

ClaimOwner(login: str, id: int)

One forge account on a claim report, as author or owner.

ATTRIBUTE DESCRIPTION
login

The forge's canonical login spelling.

TYPE: str

id

The forge's immutable numeric account id.

TYPE: int

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> ClaimOwner

Build from a decoded author or owners entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> ClaimOwner:
    """Build from a decoded `author` or `owners` entry."""
    return cls(login=_need(data, "login", _CLAIM), id=_need(data, "id", _CLAIM))

ClaimReport dataclass

ClaimReport(package: str, name: str, status: str, forge: str, transport: str, credential_kind: str, push_credential_kind: str | None, author: ClaimOwner | None, author_identity_source: str | None, owners: tuple[ClaimOwner, ...], owner_identity_source: str, branch: str, pull_request_url: str | None, pull_request_number: int | None, fork: str | None, written_paths: tuple[str, ...], capability_checks: tuple[CapabilityCheck, ...])

ocx package claim — the index entry a claim rendered (C-060).

Payload is bare (D11), seventeen keys, every one always on the wire. A package that is already claimed never produces this: ocx exits 65 with an error envelope instead, which error_envelope(err) recovers.

ATTRIBUTE DESCRIPTION
package

The claimed <namespace>/<package>, as given.

TYPE: str

name

The logical name written into the root.

TYPE: str

status

"updated" or "unchanged" — against the open claim branch, not the committed root, so a --out run is always "updated".

TYPE: str

forge

"github" or "gitlab".

TYPE: str

transport

"api" or "git".

TYPE: str

credential_kind

"job-token", "token" or "none".

TYPE: str

push_credential_kind

"job-token", "token", "git-helper", or None under the api transport.

TYPE: str | None

author

The identity that authored the request, or None when neither the token nor the CI environment named one. Not attested — see author_identity_source.

TYPE: ClaimOwner | None

author_identity_source

"resolved" when the forge's own answer about the credential produced author, "ci-environment" when an ordinary environment read did; None exactly when author is.

TYPE: str | None

owners

The recorded owners, in order.

TYPE: tuple[ClaimOwner, ...]

owner_identity_source

"resolved", "asserted" or "ci-environment".

TYPE: str

branch

The claim branch.

TYPE: str

pull_request_url

The request's URL, when one was opened.

TYPE: str | None

pull_request_number

Its number, likewise.

TYPE: int | None

fork

The fork the request came from, when --fork was given.

TYPE: str | None

written_paths

Files written under --out; empty otherwise.

TYPE: tuple[str, ...]

capability_checks

The forge probes, in CapabilityName order.

TYPE: tuple[CapabilityCheck, ...]

from_json classmethod

from_json(raw: str) -> ClaimReport

Parse ocx --format json package claim output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> ClaimReport:
    """Parse `ocx --format json package claim` output."""
    data = _object(raw, _CLAIM)
    author = _need(data, "author", _CLAIM)
    return cls(
        package=_need(data, "package", _CLAIM),
        name=_need(data, "name", _CLAIM),
        status=_need(data, "status", _CLAIM),
        forge=_need(data, "forge", _CLAIM),
        transport=_need(data, "transport", _CLAIM),
        credential_kind=_need(data, "credential_kind", _CLAIM),
        push_credential_kind=_need(data, "push_credential_kind", _CLAIM),
        author=ClaimOwner.from_dict(_table(author)) if author is not None else None,
        author_identity_source=_need(data, "author_identity_source", _CLAIM),
        owners=tuple(ClaimOwner.from_dict(row) for row in _rows(_need(data, "owners", _CLAIM))),
        owner_identity_source=_need(data, "owner_identity_source", _CLAIM),
        branch=_need(data, "branch", _CLAIM),
        pull_request_url=_need(data, "pull_request_url", _CLAIM),
        pull_request_number=_need(data, "pull_request_number", _CLAIM),
        fork=_need(data, "fork", _CLAIM),
        written_paths=_texts(_need(data, "written_paths", _CLAIM)),
        capability_checks=tuple(
            CapabilityCheck.from_dict(row) for row in _rows(_need(data, "capability_checks", _CLAIM))
        ),
    )

CleanEntry dataclass

CleanEntry(kind: str, dry_run: bool, path: str, held_by: tuple[str, ...])

One thing ocx clean removed, or would remove.

ATTRIBUTE DESCRIPTION
kind

"object" for a store object, "temp" for a temp directory, or "consent" for a state/projects/<key>/ directory whose consent stamp was swept — the entry that revokes a project's shell activation, which is why a preview names it. Raw str (D8).

TYPE: str

dry_run

Whether this was a preview.

TYPE: bool

path

What was (or would be) removed.

TYPE: str

held_by

Project ocx.lock paths holding the entry; empty when no registered project protects it, or under --force.

TYPE: tuple[str, ...]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> CleanEntry

Build from one decoded array entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> CleanEntry:
    """Build from one decoded array entry."""
    return cls(
        kind=_need(data, "kind", _CLEAN),
        dry_run=_need(data, "dry_run", _CLEAN),
        path=_need(data, "path", _CLEAN),
        held_by=_texts(_need(data, "held_by", _CLEAN)),
    )

CommandResult dataclass

CommandResult(argv: tuple[str, ...], exit_code: int, stdout: str, stderr: str)

The outcome of one ocx process, before any typing.

What invoke() returns and what every typed method is built on.

ATTRIBUTE DESCRIPTION
argv

The argv that ran, with secrets already redacted by _process.

TYPE: tuple[str, ...]

exit_code

The exit status. int, not ExitCode: a child killed by a signal reports a code ocx never assigns.

TYPE: int

stdout

Raw stdout — JSON text for a --format json call.

TYPE: str

stderr

Captured stderr in full, redacted.

TYPE: str

ConfigSetupReport dataclass

ConfigSetupReport(status: str)

ocx config setup — the managed_config block of self setup's report.

ocx wraps the block in an envelope; the SDK flattens it, because the envelope has exactly one key and a ConfigSetupReport.managed_config.status chain says nothing .status does not.

ATTRIBUTE DESCRIPTION
status

What the run did or would do, e.g. "would_adopt".

TYPE: str

from_json classmethod

from_json(raw: str) -> ConfigSetupReport

Parse ocx --format json config setup output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> ConfigSetupReport:
    """Parse `ocx --format json config setup` output."""
    data = _object(raw, "config setup")
    managed = _table(_need(data, "managed_config", "config setup"))
    return cls(status=_need(managed, "status", "config setup"))

ConfigUpdateReport dataclass

ConfigUpdateReport(status: str, source: str | None = None, digest: str | None = None, policy: str | None = None, tag: str | None = None, fetched_at: str | None = None, paused_until: str | None = None, pinned: str | None = None, drift: Any = None, kill_switches: tuple[str, ...] = ())

ocx config update — the managed-config tier's state.

status is the only field ocx always emits; the rest describe a tier that is actually configured, and a not_configured answer carries none of them.

ATTRIBUTE DESCRIPTION
status

not_configured, already_current, updated, checked, or check_unavailable.

TYPE: str

source

The OCI reference the snapshot follows.

TYPE: str | None

digest

The snapshot's digest.

TYPE: str | None

policy

apply, notify, or manual.

TYPE: str | None

tag

The tag the source resolved to.

TYPE: str | None

fetched_at

When the snapshot was fetched.

TYPE: str | None

paused_until

When an active pause expires.

TYPE: str | None

pinned

The version a pin holds it at.

TYPE: str | None

drift

How the on-disk state diverged, carried untyped.

TYPE: Any

kill_switches

Kill switches the snapshot activates.

TYPE: tuple[str, ...]

from_json classmethod

from_json(raw: str) -> ConfigUpdateReport

Parse ocx --format json config update output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> ConfigUpdateReport:
    """Parse `ocx --format json config update` output."""
    data = _object(raw, "config update")
    return cls(
        status=_need(data, "status", "config update"),
        source=data.get("source"),
        digest=data.get("digest"),
        policy=data.get("policy"),
        tag=data.get("tag"),
        fetched_at=data.get("fetched_at"),
        paused_until=data.get("paused_until"),
        pinned=data.get("pinned"),
        drift=data.get("drift"),
        kill_switches=_texts(data.get("kill_switches")),
    )

CopiedPlatformRow dataclass

CopiedPlatformRow(platform: str, digest: str, disposition: str)

One platform copy carried over to the target (C-015, package_copy.rs:128-136).

ATTRIBUTE DESCRIPTION
platform

The platform this row describes.

TYPE: str

digest

The platform manifest's digest at the target.

TYPE: str

disposition

What happened to this platform's manifest at the target — a D8 kebab-case scalar enum: "added", "unchanged", "replaced", or "kept-not-in-source" (ocx_lib/src/publisher/copy.rs:208-219). Always present.

TYPE: str

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> CopiedPlatformRow

Build from one decoded entry of CopyReport.platforms.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> CopiedPlatformRow:
    """Build from one decoded entry of `CopyReport.platforms`."""
    return cls(
        platform=_need(data, "platform", _COPY),
        digest=_need(data, "digest", _COPY),
        disposition=_need(data, "disposition", _COPY),
    )

CopyReport dataclass

CopyReport(source: str, target: str, status: str, platforms: tuple[CopiedPlatformRow, ...], cascade_tags_written: tuple[str, ...], keep_tags_written: tuple[str, ...], referrers_copied: int, sidecars_copied: int, sidecar_conflicts: tuple[str, ...], blobs: BlobSummary, description: str | None)

ocx package copy (C-015).

Payload is bare (D11) — from_json reads the JSON root directly, no envelope to unwrap. Non-empty sidecar_conflicts means exit 65 and therefore raises; because the payload is bare, partial_report(err) returns the same document this from_json parses, so the conflict list is readable from the failure itself.

All eleven fields verified always present against package_copy.rs:97-124 — no skip_serializing_if anywhere on this struct, description included (upstream comment: "serializes as null, so the key is always there to branch on"). No D7 reordering needed: this is upstream's own field order, verbatim.

ATTRIBUTE DESCRIPTION
source

The identifier that was copied.

TYPE: str

target

Where it was copied to.

TYPE: str

status

What happened, e.g. "copied", or "planned" under dry_run=True.

TYPE: str

platforms

One row per platform manifest carried over.

TYPE: tuple[CopiedPlatformRow, ...]

cascade_tags_written

Cascading tags updated at the target.

TYPE: tuple[str, ...]

keep_tags_written

__ocx.keep.sha256-<hex> tags written at the target, one per platform manifest.

TYPE: tuple[str, ...]

referrers_copied

Referrer-API attachments carried over.

TYPE: int

sidecars_copied

Sidecar-tag attachments carried over.

TYPE: int

sidecar_conflicts

Sidecar tags that already existed at the target with different content. Non-empty means the call raised.

TYPE: tuple[str, ...]

blobs

Blob transfer counts.

TYPE: BlobSummary

description

A DescriptionOutcome value — "copied", "absent", or "skipped-dry-run" — not the description text itself. Always present (null when there is nothing to report), not absent-when-unset.

TYPE: str | None

from_json classmethod

from_json(raw: str) -> CopyReport

Parse ocx --format json package copy output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> CopyReport:
    """Parse `ocx --format json package copy` output."""
    data = _object(raw, _COPY)
    return cls(
        source=_need(data, "source", _COPY),
        target=_need(data, "target", _COPY),
        status=_need(data, "status", _COPY),
        platforms=tuple(CopiedPlatformRow.from_dict(row) for row in _rows(_need(data, "platforms", _COPY))),
        cascade_tags_written=_texts(_need(data, "cascade_tags_written", _COPY)),
        keep_tags_written=_texts(_need(data, "keep_tags_written", _COPY)),
        referrers_copied=_need(data, "referrers_copied", _COPY),
        sidecars_copied=_need(data, "sidecars_copied", _COPY),
        sidecar_conflicts=_texts(_need(data, "sidecar_conflicts", _COPY)),
        blobs=BlobSummary.from_dict(_table(_need(data, "blobs", _COPY))),
        description=_need(data, "description", _COPY),
    )

DepNode dataclass

DepNode(identifier: str, repeated: bool = False, visibility: str | None = None, dependencies: tuple[Mapping[str, Any], ...] = ())

One node of the package deps tree.

ATTRIBUTE DESCRIPTION
identifier

The pinned identifier.

TYPE: str

repeated

This node already appeared elsewhere in the tree; its children are not repeated under it.

TYPE: bool

visibility

How the parent exposes it, when the parent says.

TYPE: str | None

dependencies

Child nodes, carried untyped. The only probe available had no dependencies, so the populated child shape is UNCONFIRMED — re-model this as tuple[DepNode, ...] once a real dependency edge is recorded.

TYPE: tuple[Mapping[str, Any], ...]

ref property

This node's package, as a PackageRef.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> DepNode

Build from one decoded node of the roots array.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> DepNode:
    """Build from one decoded node of the `roots` array."""
    return cls(
        identifier=_need(data, "identifier", "package deps"),
        repeated=bool(_need(data, "repeated", "package deps")),
        visibility=_need(data, "visibility", "package deps"),
        dependencies=_rows(_need(data, "dependencies", "package deps")),
    )

DepsReport dataclass

DepsReport(roots: tuple[DepNode, ...] = ())

ocx package deps — a {"roots": [...]} wrapper, not a bare array.

from_json classmethod

from_json(raw: str) -> DepsReport

Parse ocx --format json package deps output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> DepsReport:
    """Parse `ocx --format json package deps` output."""
    data = _object(raw, "package deps")
    roots = _rows(_need(data, "roots", "package deps"))
    return cls(roots=tuple(DepNode.from_dict(row) for row in roots))

DryRunEntry dataclass

DryRunEntry(package: str, status: str, path: str | None)

One row of pull(dry_run=True) — what a real pull would do to a package.

pull_dry_run.rs:76 serializes the preview as a bare root array, so there is no envelope object and no wrapper struct here: the rows are the document (D11).

ATTRIBUTE DESCRIPTION
package

The pinned identifier the row is about.

TYPE: str

status

"cached" or "would-fetch", carried as a raw str (D8).

TYPE: str

path

Where the package already sits, or None when nothing is cached — always-present-nullable (D7), and null on exactly the would-fetch rows this preview exists to surface.

TYPE: str | None

ref property

The previewed package, as a PackageRef.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> DryRunEntry

Build from one decoded array entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> DryRunEntry:
    """Build from one decoded array entry."""
    return cls(
        package=_need(data, "package", "pull"),
        status=_need(data, "status", "pull"),
        path=_need(data, "path", "pull"),
    )

EnvEntry dataclass

EnvEntry(key: str, type: EnvEntryType, value: str, separator: str | None = None)

One [env] contribution from ocx env.

ATTRIBUTE DESCRIPTION
key

The variable name.

TYPE: str

type

How the value folds onto the parent environment.

TYPE: EnvEntryType

value

The contribution, verbatim. ocx has already resolved its own ${...} grammar by the time it reaches us.

TYPE: str

separator

The separator a list entry folds with, when its project declared one. None — always, for the other two types — takes _envmodel's default, which is ocx's own.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> EnvEntry

Build from one decoded entry of an entries array.

RAISES DESCRIPTION
ValueError

The entry declares a type this SDK cannot fold.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> EnvEntry:
    """Build from one decoded entry of an `entries` array.

    Raises:
        ValueError: The entry declares a type this SDK cannot fold.
    """
    key = _need(data, "key", "env")
    declared = _need(data, "type", "env")
    if declared not in _ENV_VALUES:
        raise ValueError(
            f"`ocx env` declared type {declared!r} for {key!r}; this SDK folds only "
            f"{', '.join(sorted(_ENV_VALUES))}. Refusing rather than guessing — a variable "
            "merged the wrong way is worse than a call that fails. Upgrade ocx-sdk if ocx "
            "grew a new type."
        )
    return cls(
        key=key,
        type=declared,
        value=_need(data, "value", "env"),
        separator=data.get("separator"),
    )

EnvReport dataclass

EnvReport(entries: tuple[EnvEntry, ...] = (), binaries: tuple[PackageBinding, ...] = (), entrypoints: tuple[PackageBinding, ...] = (), integrations: tuple[Integration, ...] = (), advisories: tuple[Advisory, ...] = (), _base: Mapping[str, str] = _EMPTY)

ocx env and ocx package env — the same five-array envelope, both tiers.

Iterating the report iterates entries: composing an environment is what it is for, and the other four arrays are description.

ATTRIBUTE DESCRIPTION
entries

The [env] contributions, in declaration order.

TYPE: tuple[EnvEntry, ...]

binaries

Executables the packages put on PATH. Pre-1.0, may break.

TYPE: tuple[PackageBinding, ...]

entrypoints

Named entrypoints the packages declare. Pre-1.0, may break.

TYPE: tuple[PackageBinding, ...]

integrations

Tool integrations, payloads untyped. Pre-1.0, may break.

TYPE: tuple[Integration, ...]

advisories

Warnings about the composition. Pre-1.0, may break.

TYPE: tuple[Advisory, ...]

The four non-entry arrays carry ocx's package which-style caveat: they are not on ocx's durable-anchor list, so their shape may change in a minor release. entries is the part the SDK depends on.

Example
>>> wire = '{"entries": [{"key": "TOOL", "type": "constant", "value": "1"}],'
>>> wire += ' "binaries": [], "entrypoints": [], "integrations": [], "advisories": []}'
>>> [entry.key for entry in EnvReport.from_json(wire)]
['TOOL']

compose

compose(base: Mapping[str, str] | None = None) -> ComposedEnv

Fold these entries onto an environment.

PARAMETER DESCRIPTION
base

What to fold onto. Defaults to the environment the producing call ran under, so composing a report from a hermetic handle stays hermetic.

TYPE: Mapping[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
ComposedEnv

The merged environment. .mapping is the non-invasive form;

ComposedEnv

.activate() applies it to the whole process.

RAISES DESCRIPTION
ValueError

Two list contributions to one key disagree about the separator, or a value is edged by the one it folds with (_envmodel.merge). ocx validates its own [env] before emitting it, so a report parsed from ocx env should not reach that; a hand-built one can.

Source code in src/ocx_sdk/_results.py
def compose(self, base: Mapping[str, str] | None = None) -> ComposedEnv:
    """Fold these entries onto an environment.

    Args:
        base: What to fold onto. Defaults to the environment the producing
            call ran under, so composing a report from a hermetic handle
            stays hermetic.

    Returns:
        The merged environment. `.mapping` is the non-invasive form;
        `.activate()` applies it to the whole process.

    Raises:
        ValueError: Two list contributions to one key disagree about the
            separator, or a value is edged by the one it folds with
            (`_envmodel.merge`). ocx validates its own `[env]` before
            emitting it, so a report parsed from `ocx env` should not
            reach that; a hand-built one can.
    """
    # Function-local: the recorded `_results -> _envmodel` edge, kept out of
    # import time so neither module has to be the other's prerequisite.
    from ._envmodel import merge

    entries = ((entry.key, _ENV_VALUES[entry.type](entry.value, entry.separator)) for entry in self.entries)
    return merge(self._base if base is None else base, entries)

from_json classmethod

from_json(raw: str, *, base: Mapping[str, str] = _EMPTY) -> EnvReport

Parse ocx --format json env (or package env) output.

PARAMETER DESCRIPTION
raw

Captured stdout.

TYPE: str

base

The environment the call ran under, carried for compose. Snapshotted, so a later mutation of the source cannot rewrite what an already-parsed report composes onto.

TYPE: Mapping[str, str] DEFAULT: _EMPTY

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str, *, base: Mapping[str, str] = _EMPTY) -> EnvReport:
    """Parse `ocx --format json env` (or `package env`) output.

    Args:
        raw: Captured stdout.
        base: The environment the call ran under, carried for `compose`.
            Snapshotted, so a later mutation of the source cannot rewrite
            what an already-parsed report composes onto.
    """
    data = _object(raw, "env")
    return cls(
        entries=tuple(EnvEntry.from_dict(row) for row in _rows(_need(data, "entries", "env"))),
        binaries=tuple(PackageBinding.from_dict(row) for row in _rows(_need(data, "binaries", "env"))),
        entrypoints=tuple(PackageBinding.from_dict(row) for row in _rows(_need(data, "entrypoints", "env"))),
        integrations=tuple(Integration.from_dict(row) for row in _rows(_need(data, "integrations", "env"))),
        advisories=tuple(Advisory.from_dict(row) for row in _rows(_need(data, "advisories", "env"))),
        _base=MappingProxyType(dict(base)),
    )

ErrorEnvelope dataclass

ErrorEnvelope(schema_version: int, command: str, exit_code: int, kind: str, message: str, detail: str | None = None, remediation: str | None = None, context: Mapping[str, Any] = _EMPTY)

The structured failure ocx prints under --format json (C-S1-1).

{schema_version, command, exit_code, error: {kind, detail?, message, remediation?, context}} — a contract frozen separately from the reports schema, and printed on stdout only when the failing command wrote no report of its own. error_envelope(err) is how a caller reaches it.

The exit code is still the category (_errors): kind restates it as ocx's own vocabulary, and detail — when present — is the fine-grained slug to branch on. message is prose, free to be reworded.

ATTRIBUTE DESCRIPTION
schema_version

The envelope's own version; 1 for every 0.6 binary.

TYPE: int

command

The canonical command string, e.g. "package claim".

TYPE: str

exit_code

The exit status the process returned.

TYPE: int

kind

The coarse category, snake_case ("data_error", "auth_error", ...).

TYPE: str

message

The outermost message of the error chain.

TYPE: str

detail

The fine-grained variant slug, when ocx assigned one.

TYPE: str | None

remediation

A remediation hint. Reserved upstream and never emitted by a 0.6 binary; carried so a consumer treating it as optional keeps working when it appears.

TYPE: str | None

context

Structured context (identifiers, digests, URLs), untyped.

TYPE: Mapping[str, Any]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> ErrorEnvelope

Build from a decoded envelope, reading across its two levels.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> ErrorEnvelope:
    """Build from a decoded envelope, reading across its two levels."""
    error = _table(_need(data, _ERROR, _ENVELOPE))
    return cls(
        schema_version=_need(data, "schema_version", _ENVELOPE),
        command=_need(data, "command", _ENVELOPE),
        exit_code=_need(data, "exit_code", _ENVELOPE),
        kind=_need(error, "kind", _ENVELOPE),
        message=_need(error, "message", _ENVELOPE),
        detail=error.get("detail"),
        remediation=error.get("remediation"),
        context=_table(_need(error, "context", _ENVELOPE)),
    )

GroupStatus dataclass

GroupStatus(tools: Mapping[str, ToolBinding] = _EMPTY, env: Mapping[str, Mapping[str, Any]] = _EMPTY)

One group of ocx status.

ATTRIBUTE DESCRIPTION
tools

Binding name to its binding state.

TYPE: Mapping[str, ToolBinding]

env

The group's [env] table, keyed by variable name, carried through untyped. It is keyed here ({KEY: {type, value}}) rather than the array form ocx env emits, and no probe has yet seen it populated — so it stays a pass-through instead of a struct built on an unobserved shape.

TYPE: Mapping[str, Mapping[str, Any]]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> GroupStatus

Build from one decoded entry of the groups object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> GroupStatus:
    """Build from one decoded entry of the `groups` object."""
    tools = {name: ToolBinding.from_dict(entry) for name, entry in _table(_need(data, "tools", "status")).items()}
    env = {key: _table(entry) for key, entry in _table(_need(data, "env", "status")).items()}
    return cls(tools=MappingProxyType(tools), env=MappingProxyType(env))

IndexFinding dataclass

IndexFinding(tag: str, finding: str, committed: str | None = None, live: str | None = None)

Where the public index disagrees with the registry about an alias.

Internally tagged on finding: stale carries the two digests, and not-committed carries only the tag. Reported for ocx.sh/... packages alone, and never fixed by repair — announcing the tag is what fixes it.

ATTRIBUTE DESCRIPTION
tag

The rolling tag.

TYPE: str

finding

"stale" or "not-committed", raw str (D8).

TYPE: str

committed

The digest the index committed, under stale.

TYPE: str | None

live

The digest the alias points at today, under stale.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> IndexFinding

Build from one decoded index_findings entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> IndexFinding:
    """Build from one decoded `index_findings` entry."""
    return cls(
        tag=_need(data, "tag", _CASCADE),
        finding=_need(data, "finding", _CASCADE),
        committed=data.get("committed"),
        live=data.get("live"),
    )

InspectedPackage dataclass

InspectedPackage(name: str, identifier: str, candidates: tuple[Candidate, ...] = (), pinned_identifier: str | None = None, pinned_digest: str | None = None, platform: Mapping[str, Any] | None = None, metadata: Mapping[str, Any] | None = None, layers: tuple[Mapping[str, Any], ...] = (), resolution: Mapping[str, Any] | None = None, closure: Mapping[str, Any] | None = None)

One package row of inspect or package inspect.

name reads differently per tier: a binding name at the toolchain tier, the full requested identifier at the package tier.

Everything from pinned_identifier down appears only under --resolve or --closure. resolution and closure stay mappings: both are deep trees whose interesting parts (closure.surface, closure.conflicts) are still moving upstream, and modelling a tree we would have to re-model on the next ocx release buys nothing v0.1 needs.

ATTRIBUTE DESCRIPTION
name

Binding name, or the requested identifier at the package tier.

TYPE: str

identifier

The identifier as requested.

TYPE: str

candidates

One entry per platform ocx found.

TYPE: tuple[Candidate, ...]

pinned_identifier

--resolve: the identifier with its digest.

TYPE: str | None

pinned_digest

--resolve: the digest alone.

TYPE: str | None

platform

--resolve: the resolved platform object.

TYPE: Mapping[str, Any] | None

metadata

--resolve: the package's metadata.json.

TYPE: Mapping[str, Any] | None

layers

--resolve: the layer descriptors.

TYPE: tuple[Mapping[str, Any], ...]

resolution

--resolve: the pin and the walk that produced it.

TYPE: Mapping[str, Any] | None

closure

--closure: dependencies, surface, and conflicts.

TYPE: Mapping[str, Any] | None

ref property

The pinned identifier when resolved, else the requested one.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> InspectedPackage

Build from one decoded entry of the packages array.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> InspectedPackage:
    """Build from one decoded entry of the `packages` array."""
    return cls(
        name=_need(data, "name", "inspect"),
        identifier=_need(data, "identifier", "inspect"),
        candidates=tuple(Candidate.from_dict(row) for row in _rows(data.get("candidates"))),
        pinned_identifier=data.get("pinned_identifier"),
        pinned_digest=data.get("pinned_digest"),
        platform=_opt_table(data.get("platform")),
        metadata=_opt_table(data.get("metadata")),
        layers=_rows(data.get("layers")),
        resolution=_opt_table(data.get("resolution")),
        closure=_opt_table(data.get("closure")),
    )

InspectReport dataclass

InspectReport(packages: tuple[InspectedPackage, ...] = (), env: tuple[EnvEntry, ...] = (), platform: str | None = None)

ocx inspect and ocx package inspect — the same envelope, both tiers.

ATTRIBUTE DESCRIPTION
packages

One row per requested package or declared binding.

TYPE: tuple[InspectedPackage, ...]

env

The [env] entries those packages contribute, in order. Duplicates are allowed and meaningful — the merge folds them.

TYPE: tuple[EnvEntry, ...]

platform

The platform resolution ran against, under --resolve.

TYPE: str | None

from_json classmethod

from_json(raw: str) -> InspectReport

Parse ocx --format json inspect (or package inspect) output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> InspectReport:
    """Parse `ocx --format json inspect` (or `package inspect`) output."""
    data = _object(raw, "inspect")
    return cls(
        packages=tuple(InspectedPackage.from_dict(row) for row in _rows(_need(data, "packages", "inspect"))),
        env=tuple(EnvEntry.from_dict(row) for row in _rows(_need(data, "env", "inspect"))),
        platform=data.get("platform"),
    )

InstalledPackage dataclass

InstalledPackage(identifier: str, path: str | None, metadata: Mapping[str, Any] = _EMPTY)

One package package install or package select put in place.

ATTRIBUTE DESCRIPTION
identifier

The pinned identifier, <as given>@sha256:....

TYPE: str

path

The symlink that now points at it — candidates/<tag> after an install, current after a select — or None when nothing was materialized. Always-present-nullable (install.rs:23).

TYPE: str | None

metadata

The package's metadata.json, carried untyped.

TYPE: Mapping[str, Any]

ref property

The pinned identifier, carrying its metadata.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> InstalledPackage

Build from one decoded value of the keyed object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> InstalledPackage:
    """Build from one decoded value of the keyed object."""
    return cls(
        identifier=_need(data, "identifier", "package install"),
        path=_need(data, "path", "package install"),
        metadata=_table(_need(data, "metadata", "package install")),
    )

InstallReport dataclass

InstallReport(packages: Mapping[str, InstalledPackage] = _EMPTY)

ocx package install and ocx package select — the same keyed object.

Keys are the identifiers as given; the values carry the pinned form. That asymmetry is the point: a caller can look up what it asked for without knowing what it resolved to.

ATTRIBUTE DESCRIPTION
packages

Requested identifier to what got installed.

TYPE: Mapping[str, InstalledPackage]

from_json classmethod

from_json(raw: str) -> InstallReport

Parse ocx --format json package install (or select) output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> InstallReport:
    """Parse `ocx --format json package install` (or `select`) output."""
    data = _object(raw, "package install")
    packages = {key: InstalledPackage.from_dict(_table(value)) for key, value in data.items()}
    return cls(packages=MappingProxyType(packages))

Integration dataclass

Integration(namespace: str, package: str | None = None, payload: Mapping[str, Any] = _EMPTY)

A tool integration a package declares, with its payload carried untyped.

ATTRIBUTE DESCRIPTION
namespace

The integration namespace the package declares.

TYPE: str

package

The declaring package, or None when ocx could not attribute the contribution — env.rs:137 skips the key for the same reason PackageBinding.package does (D7).

TYPE: str | None

payload

The interpolated payload, uninterpreted.

TYPE: Mapping[str, Any]

ref property

ref: PackageRef | None

The declaring package, as a PackageRef, or None when unattributed.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> Integration

Build from one decoded integrations entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Integration:
    """Build from one decoded `integrations` entry."""
    return cls(
        namespace=_need(data, "namespace", "env"),
        package=data.get("package"),
        payload=_table(_need(data, "payload", "env")),
    )

ListingSummary dataclass

ListingSummary(status: str, verification: str, exit_code: int, total: int, verified: int, unverified: int, refused: int)

The summary block of an SbomListingReport (C-014).

sbom.rs:75. All seven fields are always present.

ATTRIBUTE DESCRIPTION
status

"success" or "partial_failure" — a listing can refuse individual candidates and still exit 0 (D10's note that this differs from a swept sign/attest, which exits non-zero on a partial failure).

TYPE: str

verification

"verified" or "unverified", carried as raw str (D8) — ocx's ListingVerification, lowercase on the wire.

TYPE: str

exit_code

The process's own exit code.

TYPE: int

total

Candidates examined.

TYPE: int

verified

Candidates whose signature verified.

TYPE: int

unverified

Candidates attached without a verifying signature.

TYPE: int

refused

Candidates rejected outright — see RefusedEntry.

TYPE: int

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> ListingSummary

Build from the decoded summary object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> ListingSummary:
    """Build from the decoded `summary` object."""
    return cls(
        status=_need(data, "status", _SBOM),
        verification=_need(data, "verification", _SBOM),
        exit_code=_need(data, "exit_code", _SBOM),
        total=_need(data, "total", _SBOM),
        verified=_need(data, "verified", _SBOM),
        unverified=_need(data, "unverified", _SBOM),
        refused=_need(data, "refused", _SBOM),
    )

LockStatus dataclass

LockStatus(present: bool = False, current: bool = False, lock_version: int | None = None, declaration_hash: str | None = None, declaration_hash_expected: str | None = None, generated_by: str | None = None, generated_at: str | None = None, error: str | None = None)

The lock block of ocx status.

A lock file that is broken or unreadable is payload, not failure: ocx still exits 0 and reports error here. Callers check current, not the exit code.

ATTRIBUTE DESCRIPTION
present

A lock file exists.

TYPE: bool

current

It matches the declaration it was generated from.

TYPE: bool

lock_version

Lock file format version.

TYPE: int | None

declaration_hash

Hash recorded in the lock file.

TYPE: str | None

declaration_hash_expected

Hash of the declaration as it reads now. Differs from declaration_hash exactly when current is false.

TYPE: str | None

generated_by

The ocx version that wrote the lock file.

TYPE: str | None

generated_at

When it did, as ocx spelled it.

TYPE: str | None

error

Why the lock file could not be read, when it could not be.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> LockStatus

Build from the decoded lock object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> LockStatus:
    """Build from the decoded `lock` object."""
    return cls(
        present=bool(_need(data, "present", "status")),
        current=bool(data.get("current")),
        lock_version=data.get("lock_version"),
        declaration_hash=data.get("declaration_hash"),
        declaration_hash_expected=_need(data, "declaration_hash_expected", "status"),
        generated_by=data.get("generated_by"),
        generated_at=data.get("generated_at"),
        error=data.get("error"),
    )

LoginResult dataclass

LoginResult(registry: str, username: str)

ocx login — which registry, as whom.

from_json classmethod

from_json(raw: str) -> LoginResult

Parse ocx --format json login output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> LoginResult:
    """Parse `ocx --format json login` output."""
    data = _object(raw, "login")
    return cls(registry=_need(data, "registry", "login"), username=_need(data, "username", "login"))

LogoutResult dataclass

LogoutResult(registry: str)

ocx logout — which registry. Always succeeds, even if nothing was stored.

from_json classmethod

from_json(raw: str) -> LogoutResult

Parse ocx --format json logout output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> LogoutResult:
    """Parse `ocx --format json logout` output."""
    return cls(registry=_need(_object(raw, "logout"), "registry", "logout"))

PackageBinding dataclass

PackageBinding(name: str, package: str | None = None)

A name a package provides — the row shape binaries and entrypoints share.

ATTRIBUTE DESCRIPTION
name

The binary or entrypoint name the package claims.

TYPE: str

package

The package that claimed it, or None when ocx could not attribute the claim. Absent-when-unattributed (D7): env.rs:100 skips the key, and its own doc reserves None for a future source with no clean attribution.

TYPE: str | None

ref property

ref: PackageRef | None

The providing package, as a PackageRef, or None when unattributed.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> PackageBinding

Build from one decoded binaries or entrypoints entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> PackageBinding:
    """Build from one decoded `binaries` or `entrypoints` entry."""
    return cls(name=_need(data, "name", "env"), package=data.get("package"))

PackageDescription dataclass

PackageDescription(title: str | None, description: str | None, keywords: str | None)

One package's description metadata, from package description pull (C-021, D4).

api/data/package_description.rs's Inner — every field is optional upstream, but carries no skip_serializing_if, so all three keys are always present and null when unset — not absent-when-empty. D7 therefore wants str | None with no Python default, parsed via _need, not data.get(). An all-None instance is still a real, valid value either way: it is distinct from the None entry InfoResult uses when the registry holds no description at all.

ATTRIBUTE DESCRIPTION
title

The package's display title.

TYPE: str | None

description

Its prose description.

TYPE: str | None

keywords

A single delimited string upstream, not a list — do not split it into a collection here.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> PackageDescription

Build from one non-null value of package description pull's keyed object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> PackageDescription:
    """Build from one non-`null` value of `package description pull`'s keyed object."""
    return cls(
        title=_need(data, "title", _DESCRIPTION_PULL),
        description=_need(data, "description", _DESCRIPTION_PULL),
        keywords=_need(data, "keywords", _DESCRIPTION_PULL),
    )

PullReport dataclass

PullReport(packages: Mapping[str, WhichResult] = _EMPTY, advisories: tuple[Mapping[str, Any], ...] = ())

ocx pull at the project tier — paths plus advisories.

The wire object mixes both: identifiers key {path, kind} values, and a sibling advisories key sits among them. An identifier is always at least <repo>/<name>, so it can never collide with that key.

ATTRIBUTE DESCRIPTION
packages

Pinned identifier to where it landed.

TYPE: Mapping[str, WhichResult]

advisories

Warnings, carried untyped — no probe has seen a populated one, and ocx env's advisory shape is not known to apply here.

TYPE: tuple[Mapping[str, Any], ...]

from_json classmethod

from_json(raw: str) -> PullReport

Parse ocx --format json pull output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> PullReport:
    """Parse `ocx --format json pull` output."""
    data = _object(raw, "pull")
    packages = {key: WhichResult.from_dict(_table(value)) for key, value in data.items() if key != _ADVISORIES}
    return cls(packages=MappingProxyType(packages), advisories=_rows(_need(data, _ADVISORIES, "pull")))

PushResult dataclass

PushResult(identifier: str, status: str, manifest_digest: str, cascade_tags_written: tuple[str, ...], keep_tags_written: tuple[str, ...], layers: Mapping[str, Any], platform_digests: Mapping[str, str] = _EMPTY, signatures: tuple[SignedPlatformReport, ...] = (), attestation: AttestationOutcome | None = None)

ocx package push — what landed in the registry.

ATTRIBUTE DESCRIPTION
identifier

What was pushed.

TYPE: str

status

What happened, e.g. "pushed".

TYPE: str

manifest_digest

Digest of the manifest that was written.

TYPE: str

cascade_tags_written

Cascading tags updated by the push.

TYPE: tuple[str, ...]

keep_tags_written

The __ocx.keep.sha256-<hex> tags written, one per platform manifest — push's --keep-tag flag, which is on by default. Named for the wire key ocx 0.6 emits (C-005); 0.5 called it canonical_tags_written and wrote a sha256.<hex> tag, so a 0.1 call site reading the old attribute must be edited.

TYPE: tuple[str, ...]

layers

Layer counts (mounted, uploaded, verified).

TYPE: Mapping[str, Any]

platform_digests

Manifest digest per pushed platform (C-005). Absent-when-unused (D7).

TYPE: Mapping[str, str]

signatures

One row per platform's signing outcome, when push(..., sign=True) was requested (C-005, C-018). Absent-when-unused (D7).

TYPE: tuple[SignedPlatformReport, ...]

attestation

The push's attestation outcome, when one was requested (C-005, C-019). Absent-when-unused (D7).

TYPE: AttestationOutcome | None

ref property

The pushed package, as a PackageRef.

from_json classmethod

from_json(raw: str) -> PushResult

Parse ocx --format json package push output.

The payload is bare (D11) — the report sits at the JSON root, with no envelope to unwrap. A push that lands and then fails to sign exits non-zero carrying this same document, so partial_report(err) feeds straight back into this parser (D10, C-009).

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> PushResult:
    """Parse `ocx --format json package push` output.

    The payload is **bare** (D11) — the report sits at the JSON root, with
    no envelope to unwrap. A push that lands and then fails to sign exits
    non-zero carrying this same document, so `partial_report(err)` feeds
    straight back into this parser (D10, C-009).
    """
    data = _object(raw, _PUSH)
    attestation = data.get("attestation")
    return cls(
        identifier=_need(data, "identifier", _PUSH),
        status=_need(data, "status", _PUSH),
        manifest_digest=_need(data, "manifest_digest", _PUSH),
        cascade_tags_written=_texts(_need(data, "cascade_tags_written", _PUSH)),
        keep_tags_written=_texts(_need(data, "keep_tags_written", _PUSH)),
        layers=_table(_need(data, "layers", _PUSH)),
        platform_digests=_table(data.get("platform_digests")),
        signatures=tuple(SignedPlatformReport.from_dict(row) for row in _rows(data.get("signatures"))),
        attestation=AttestationOutcome.from_dict(_table(attestation)) if attestation is not None else None,
    )

RefusedEntry dataclass

RefusedEntry(referrer_digest: str, reason: str, reason_kind: str)

One candidate sbom examined and rejected (C-014).

All three fields always present (sbom.rs:192-205).

ATTRIBUTE DESCRIPTION
referrer_digest

The rejected candidate's own digest.

TYPE: str

reason

English prose explaining the refusal — free to be reworded upstream (PKG-25); branch on reason_kind, never on this text.

TYPE: str

reason_kind

The frozen, machine-branchable slug to switch on.

TYPE: str

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> RefusedEntry

Build from one decoded entry of SbomListingReport.refused.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> RefusedEntry:
    """Build from one decoded entry of `SbomListingReport.refused`."""
    return cls(
        referrer_digest=_need(data, "referrer_digest", _SBOM),
        reason=_need(data, "reason", _SBOM),
        reason_kind=_need(data, "reason_kind", _SBOM),
    )

RemovalResult dataclass

RemovalResult(package: str, status: str, path: str | None)

One row of package uninstall or package deselect.

Both commands return a bare array rather than a keyed object — the only two package-tier mutators that do.

ATTRIBUTE DESCRIPTION
package

The identifier as given.

TYPE: str

status

What happened, e.g. "removed".

TYPE: str

path

The symlink that went away, or None when there was none — always-present-nullable (removed.rs:40), which every status: "absent" row answers with.

TYPE: str | None

ref property

The removed package, as a PackageRef.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> RemovalResult

Build from one decoded array entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> RemovalResult:
    """Build from one decoded array entry."""
    return cls(
        package=_need(data, "package", "package uninstall"),
        status=_need(data, "status", "package uninstall"),
        path=_need(data, "path", "package uninstall"),
    )

RepairEntry dataclass

RepairEntry(report: CascadeReport, planned: tuple[Mapping[str, Any], ...], outcomes: tuple[RepairOutcome, ...], announce_tags: tuple[str, ...])

One package's cascade repair run.

ATTRIBUTE DESCRIPTION
report

The audit the plan was computed from — the same shape check reports.

TYPE: CascadeReport

planned

The alias indexes the run would write (or wrote), each {tag, index, observed_digest, referenced_digests, reasons}, carried untyped: the index is a whole OCI image index.

TYPE: tuple[Mapping[str, Any], ...]

outcomes

What happened per tag. Empty on a dry run.

TYPE: tuple[RepairOutcome, ...]

announce_tags

The rolling tags this run moved or created — the lines --announce-tags writes, echoed for a JSON consumer.

TYPE: tuple[str, ...]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> RepairEntry

Build from one decoded entries entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> RepairEntry:
    """Build from one decoded `entries` entry."""
    return cls(
        report=CascadeReport.from_dict(_table(_need(data, "report", _CASCADE_REPAIR))),
        planned=_rows(_need(data, "planned", _CASCADE_REPAIR)),
        outcomes=tuple(RepairOutcome.from_dict(row) for row in _rows(_need(data, "outcomes", _CASCADE_REPAIR))),
        announce_tags=_texts(_need(data, "announce_tags", _CASCADE_REPAIR)),
    )

RepairOutcome dataclass

RepairOutcome(tag: str, outcome: str, detail: Mapping[str, Any])

What cascade repair did to one rolling tag.

The wire nests a WriteOutcome object under outcome, internally tagged on its own outcome key: written (with digest, verified, and dropped when child digests were pruned), refused (an unrepairable finding's tag/reason/digest), raced (expected, live) or failed (message). First-cut depth: the discriminator is typed and the variant's fields ride along untyped.

ATTRIBUTE DESCRIPTION
tag

The rolling tag.

TYPE: str

outcome

"written", "refused", "raced" or "failed", raw str (D8).

TYPE: str

detail

The whole WriteOutcome object, outcome key included.

TYPE: Mapping[str, Any]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> RepairOutcome

Build from one decoded outcomes entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> RepairOutcome:
    """Build from one decoded `outcomes` entry."""
    detail = _table(_need(data, "outcome", _CASCADE_REPAIR))
    return cls(
        tag=_need(data, "tag", _CASCADE_REPAIR), outcome=_need(detail, "outcome", _CASCADE_REPAIR), detail=detail
    )

SbomEntry dataclass

SbomEntry(predicate_type: str, verified: bool, shadowed: bool, subject_digest: str, referrer_digest: str, certificate_identity: str | None = None, certificate_oidc_issuer: str | None = None, signed_at: str | None = None, summary: SbomSummaryOut | None = None)

One SBOM document sbom listed (C-014).

Field list verified against sbom.rs:104-162.

ATTRIBUTE DESCRIPTION
predicate_type

The SBOM's predicate type URI (CycloneDX, SPDX, ...).

TYPE: str

verified

Whether a signature was verified over this document. False means it is attached raw, with no identity behind it — this is ocx policy, not a cosign or CycloneDX term.

TYPE: bool

shadowed

Whether a platform-level SBOM of the same predicate_type supersedes this index-level one. Always present; False is a true claim, not an absence.

TYPE: bool

subject_digest

The manifest digest this SBOM describes.

TYPE: str

referrer_digest

This SBOM document's own digest. Not always a manifest digest — for a verified .att sidecar it is a layer blob digest (sbom.rs:128-148); feeding it to a manifest fetch 404s. Same caveat SignatureEntry/VerificationReport carry for their own referrer_digest.

TYPE: str

certificate_identity

The Fulcio certificate's identity, when verified and the signature is keyless.

TYPE: str | None

certificate_oidc_issuer

The Fulcio certificate's OIDC issuer, when verified and the signature is keyless.

TYPE: str | None

signed_at

When the signature was produced, as ocx spelled it, when verified.

TYPE: str | None

summary

Component counts, when the call carried --summary and this entry parsed successfully as CycloneDX 1.5-1.7 — see SbomSummaryOut. None otherwise; the listing itself still works without --summary, it just leaves this unpopulated.

TYPE: SbomSummaryOut | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SbomEntry

Build from one decoded entry of SbomListingReport.entries.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SbomEntry:
    """Build from one decoded entry of `SbomListingReport.entries`."""
    summary = data.get("summary")
    return cls(
        predicate_type=_need(data, "predicate_type", _SBOM),
        verified=_need(data, "verified", _SBOM),
        shadowed=_need(data, "shadowed", _SBOM),
        subject_digest=_need(data, "subject_digest", _SBOM),
        referrer_digest=_need(data, "referrer_digest", _SBOM),
        certificate_identity=data.get("certificate_identity"),
        certificate_oidc_issuer=data.get("certificate_oidc_issuer"),
        signed_at=data.get("signed_at"),
        summary=SbomSummaryOut.from_dict(_table(summary)) if summary is not None else None,
    )

SbomListingReport dataclass

SbomListingReport(summary: ListingSummary, entries: tuple[SbomEntry, ...], refused: tuple[RefusedEntry, ...])

ocx package sbom — the full listing (C-014).

Payload is enveloped (D11): from_json unwraps data. Exits 0 even when candidates were refused — check summary.status, not the exit code.

ATTRIBUTE DESCRIPTION
summary

Counts and overall status.

TYPE: ListingSummary

entries

SBOM documents ocx listed. A plain Vec upstream with no skip, so always present, not absent-when-empty.

TYPE: tuple[SbomEntry, ...]

refused

Candidates ocx rejected outright. Same as entries.

TYPE: tuple[RefusedEntry, ...]

from_json classmethod

from_json(raw: str) -> SbomListingReport

Parse ocx --format json package sbom output (full listing).

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> SbomListingReport:
    """Parse `ocx --format json package sbom` output (full listing)."""
    data = _envelope(raw, _SBOM)
    return cls(
        summary=ListingSummary.from_dict(_table(_need(data, "summary", _SBOM))),
        entries=tuple(SbomEntry.from_dict(row) for row in _rows(_need(data, "entries", _SBOM))),
        refused=tuple(RefusedEntry.from_dict(row) for row in _rows(_need(data, "refused", _SBOM))),
    )

SbomSummaryOut dataclass

SbomSummaryOut(spec_version: str, component_count: int, serial_number: str | None = None, top_level_component: str | None = None)

CycloneDX component counts for one SBOM entry, under --summary (C-014, sbom.rs:166-177).

Not a second top-level shape. --summary does not change sbom's JSON root — it populates this struct per-entry, at SbomEntry.summary, for each entry that parsed successfully as CycloneDX 1.5-1.7. Nothing like ListingSummary's seven fields: sbom() needs no overload, and this is built with from_dict, never from_json, because it never sits at a JSON root.

ATTRIBUTE DESCRIPTION
spec_version

The CycloneDX spec version this document declares.

TYPE: str

component_count

How many components the document lists.

TYPE: int

serial_number

The document's serial number, when it declares one.

TYPE: str | None

top_level_component

The root component's name, when the document declares one.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SbomSummaryOut

Build from the decoded summary object of one SbomEntry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SbomSummaryOut:
    """Build from the decoded `summary` object of one `SbomEntry`."""
    return cls(
        spec_version=_need(data, "spec_version", _SBOM),
        component_count=_need(data, "component_count", _SBOM),
        serial_number=data.get("serial_number"),
        top_level_component=data.get("top_level_component"),
    )

SignatureEntry dataclass

SignatureEntry(signature_format: str, discovery_method: str, key_backend: str, referrer_digest: str, certificate_identity: str | None = None, certificate_oidc_issuer: str | None = None, signed_at: str | None = None, rekor_log_index: int | None = None)

One signature verify found on the inspected object (C-012).

Field list verified against verification.rs:50-85.

ATTRIBUTE DESCRIPTION
signature_format

Which format this entry is, carried as raw str (D8).

TYPE: str

discovery_method

How ocx found it — referrer API or sidecar tag, carried as raw str (D8).

TYPE: str

key_backend

Which key backend verified it, carried as raw str (D8).

TYPE: str

referrer_digest

This signature's own digest. Not always a manifest digest — for a simplesigning sidecar it is a layer blob digest; feeding it to a manifest fetch 404s.

TYPE: str

certificate_identity

The Fulcio certificate's identity, when the signature is keyless.

TYPE: str | None

certificate_oidc_issuer

The Fulcio certificate's OIDC issuer, when the signature is keyless.

TYPE: str | None

signed_at

When the signature was produced, as ocx spelled it, when present.

TYPE: str | None

rekor_log_index

The Rekor entry index, when present. Named differently from SignatureReport/AttestationReport's transparency_log_index — same concept, two wire names, and unlike those two structs this field is absent rather than always-present-nullable. Do not unify the handling.

TYPE: int | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SignatureEntry

Build from one decoded entry of VerificationReport.signatures.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SignatureEntry:
    """Build from one decoded entry of `VerificationReport.signatures`."""
    return cls(
        signature_format=_need(data, "signature_format", _VERIFY),
        discovery_method=_need(data, "discovery_method", _VERIFY),
        key_backend=_need(data, "key_backend", _VERIFY),
        referrer_digest=_need(data, "referrer_digest", _VERIFY),
        certificate_identity=data.get("certificate_identity"),
        certificate_oidc_issuer=data.get("certificate_oidc_issuer"),
        signed_at=data.get("signed_at"),
        rekor_log_index=data.get("rekor_log_index"),
    )

SignatureLegReport dataclass

SignatureLegReport(format: str, payload_digest: str | None = None, manifest_digest: str | None = None, error: str | None = None)

One signature format's outcome within a SignatureReport (C-011).

ATTRIBUTE DESCRIPTION
format

The signature format this leg produced — "bundle" or "simplesigning" (signature.rs:89; "both" is write-side only — it requests two legs, never labels one), carried as raw str (D8).

TYPE: str

payload_digest

Digest of the signed payload, when this leg landed.

TYPE: str | None

manifest_digest

Digest of the manifest the signature attaches to, when this leg landed.

TYPE: str | None

error

Why this leg failed, when it did. A --signature-format both run where one leg lands and one fails names the failed leg here (D10) — recover the whole report with partial_report(err).

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SignatureLegReport

Build from one decoded entry of SignatureReport.legs.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SignatureLegReport:
    """Build from one decoded entry of `SignatureReport.legs`."""
    return cls(
        format=_need(data, "format", _SIGN),
        payload_digest=data.get("payload_digest"),
        manifest_digest=data.get("manifest_digest"),
        error=data.get("error"),
    )

SignatureReport dataclass

SignatureReport(identifier: str, subject_digest: str, legs: tuple[SignatureLegReport, ...], platform: str, signer: str, certificate_identity: str, certificate_oidc_issuer: str, key_backend: str, transparency_log_index: int | None, public_key_hint: str | None = None)

ocx package sign on a single reference — no tag sweep (C-011).

Payload is enveloped (D11): from_json unwraps data via _envelope before parsing. A --signature-format both run where one leg lands and one fails exits non-zero carrying a full report — recover it with partial_report(err) and this same from_json (D10); legs names which leg died through its error field.

Per D7's field-order rule, public_key_hint moves to the end even though upstream orders it before transparency_log_index — transcribing that order verbatim would put a defaulted field before a required one.

ATTRIBUTE DESCRIPTION
identifier

The identifier that was signed.

TYPE: str

subject_digest

The manifest digest the signature covers.

TYPE: str

legs

One row per signature format requested.

TYPE: tuple[SignatureLegReport, ...]

platform

The signed platform.

TYPE: str

signer

The signing mode label — never an identity and never a key reference. "keyless-fulcio" under keyless signing (signature.rs:159-162), else key_backend's own backend label (awskms, file, ...). Deliberately spelled differently from key_backend, which reads the plain "keyless" in that same case (signature.rs:155-157): upstream's own comment notes that reusing "keyless" here too would leave no field that actually names the mechanism.

TYPE: str

certificate_identity

The Fulcio certificate's identity. Plain String upstream (signature.rs:59) — always emitted, on both the keyless and key-based paths.

TYPE: str

certificate_oidc_issuer

The Fulcio certificate's OIDC issuer. Always emitted (signature.rs:61), same as above.

TYPE: str

key_backend

Which key backend produced the signature, carried as raw str (D8; irregular KMS spellings live here unmodified). Always emitted (signature.rs:68); under keyless signing the value is literally "keyless", not absent.

TYPE: str

transparency_log_index

The Rekor entry index. Always present in the JSON, but None when nothing was logged.

TYPE: int | None

public_key_hint

A hint identifying the verifying key, when ocx supplied one.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SignatureReport

Build from a decoded sign report.

Used both for the envelope's data (a bare sign) and, through C-017's row-parser seam, for one SweptTagReport.report row of a swept sign.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SignatureReport:
    """Build from a decoded sign report.

    Used both for the envelope's `data` (a bare `sign`) and, through
    C-017's row-parser seam, for one `SweptTagReport.report` row of a
    swept `sign`.
    """
    return cls(
        identifier=_need(data, "identifier", _SIGN),
        subject_digest=_need(data, "subject_digest", _SIGN),
        legs=tuple(SignatureLegReport.from_dict(row) for row in _rows(_need(data, "legs", _SIGN))),
        platform=_need(data, "platform", _SIGN),
        signer=_need(data, "signer", _SIGN),
        certificate_identity=_need(data, "certificate_identity", _SIGN),
        certificate_oidc_issuer=_need(data, "certificate_oidc_issuer", _SIGN),
        key_backend=_need(data, "key_backend", _SIGN),
        transparency_log_index=_need(data, "transparency_log_index", _SIGN),
        public_key_hint=data.get("public_key_hint"),
    )

from_json classmethod

from_json(raw: str) -> SignatureReport

Parse ocx --format json package sign output (no tag sweep).

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> SignatureReport:
    """Parse `ocx --format json package sign` output (no tag sweep)."""
    return cls.from_dict(_envelope(raw, _SIGN))

SignedPlatformReport dataclass

SignedPlatformReport(platform: str, status: str, report: SignatureReport | None = None, kind: str | None = None, message: str | None = None)

One platform's signing outcome, nested in PushResult.signatures (C-018).

push.rs:115. The three optionals are absent-when-none (D7): a platform whose signing needed no more than the default leg carries report alone, and kind/message populate only when that platform's signing failed.

ATTRIBUTE DESCRIPTION
platform

The platform this row describes.

TYPE: str

status

A SweptStatus value (completed/skipped/failed), carried as raw str (D8).

TYPE: str

report

The platform's own SignatureReport, when produced.

TYPE: SignatureReport | None

kind

The machine-branchable failure slug, when this platform failed.

TYPE: str | None

message

Sanitized prose describing the failure, free to be reworded.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SignedPlatformReport

Build from one decoded entry of PushResult.signatures.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SignedPlatformReport:
    """Build from one decoded entry of `PushResult.signatures`."""
    report = data.get("report")
    return cls(
        platform=_need(data, "platform", _PUSH),
        status=_need(data, "status", _PUSH),
        report=SignatureReport.from_dict(_table(report)) if report is not None else None,
        kind=data.get("kind"),
        message=data.get("message"),
    )

SlotRow dataclass

SlotRow(tag: str, platform: Mapping[str, Any], status: str, observed: str | None, expected: str | None, source: str | None, observed_source: str | None)

One (rolling tag, platform) slot cascade check examined.

ATTRIBUTE DESCRIPTION
tag

The rolling tag — latest, 3, 3.28, or a variant name.

TYPE: str

platform

The OCI platform object of the slot, carried untyped: the published schema types it as true (anything), and no dispatch here needs its fields.

TYPE: Mapping[str, Any]

status

ok, missing, stale, orphan or duplicate — the finding, as raw str (D8). Anything but ok is what makes check exit 65.

TYPE: str

observed

The digest the alias carries for this platform, if any.

TYPE: str | None

expected

The digest the fold expects, if any.

TYPE: str | None

source

The version the expectation was folded from, e.g. "1.0.1", or None. Published as PackageVersion (a string) since 0.6.2; 0.6.1's schema named an integer Version here, a generator artifact the wire never bore out (ocx#460).

TYPE: str | None

observed_source

The version the observed digest belongs to, or None when the alias points at content no observed leaf carries.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> SlotRow

Build from one decoded rows entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SlotRow:
    """Build from one decoded `rows` entry."""
    return cls(
        tag=_need(data, "tag", _CASCADE),
        platform=_table(_need(data, "platform", _CASCADE)),
        status=_need(data, "status", _CASCADE),
        observed=_need(data, "observed", _CASCADE),
        expected=_need(data, "expected", _CASCADE),
        source=_need(data, "source", _CASCADE),
        observed_source=_need(data, "observed_source", _CASCADE),
    )

StatusReport dataclass

StatusReport(project: str, lock: LockStatus = LockStatus(), groups: Mapping[str, GroupStatus] = _EMPTY, package_settings: Mapping[str, Mapping[str, Any]] = _EMPTY)

ocx status — the project's declaration, lock, and groups.

Exits 0 even when the lock is broken, so this struct is where a caller learns something is wrong (lock.current, lock.error).

ATTRIBUTE DESCRIPTION
project

Path to the ocx.toml the report describes.

TYPE: str

lock

Lock file state.

TYPE: LockStatus

groups

Group name to its contents.

TYPE: Mapping[str, GroupStatus]

package_settings

Per-package settings, carried through untyped — they are ocx's configuration surface, not the SDK's.

TYPE: Mapping[str, Mapping[str, Any]]

from_json classmethod

from_json(raw: str) -> StatusReport

Parse ocx --format json status output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> StatusReport:
    """Parse `ocx --format json status` output."""
    data = _object(raw, "status")
    groups = {name: GroupStatus.from_dict(entry) for name, entry in _table(_need(data, "groups", "status")).items()}
    settings = {name: _table(entry) for name, entry in _table(_need(data, "package_settings", "status")).items()}
    return cls(
        project=_need(data, "project", "status"),
        lock=LockStatus.from_dict(_table(_need(data, "lock", "status"))),
        groups=MappingProxyType(groups),
        package_settings=MappingProxyType(settings),
    )

SweepReport dataclass

SweepReport(tags: tuple[SweptTagReport, ...])

ocx package sign --tags / attest --tags (or --tags-file) (C-017, D2).

Upstream is generic (sweep.rs:152, SweepReport<R> { tags: Vec<SweptTagReport<R>> }); this is its Python shape for both instantiations. Carries no command or exit_code field — both are #[serde(skip)] upstream.

Payload is enveloped (D11): the envelope's data holds {"tags": [...]}, and its exit_code — not part of data — is the process's own. A partially-failed sweep exits non-zero, so a failed row usually arrives through partial_report(err) rather than a normal return (D10); both paths parse through this same from_json.

ATTRIBUTE DESCRIPTION
tags

One row per swept tag. A plain Vec upstream with no skip, so always present, not absent-when-empty.

TYPE: tuple[SweptTagReport, ...]

from_json classmethod

from_json(raw: str, row: Callable[[Mapping[str, Any]], _SweepRow]) -> SweepReport

Parse an enveloped sweep payload from a swept sign or attest.

PARAMETER DESCRIPTION
raw

Captured stdout.

TYPE: str

row

Parses each row's report object — see SweptTagReport.from_dict.

TYPE: Callable[[Mapping[str, Any]], _SweepRow]

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str, row: Callable[[Mapping[str, Any]], _SweepRow]) -> SweepReport:
    """Parse an enveloped sweep payload from a swept `sign` or `attest`.

    Args:
        raw: Captured stdout.
        row: Parses each row's `report` object — see
            `SweptTagReport.from_dict`.
    """
    data = _envelope(raw, _SWEEP)
    tags = _rows(_need(data, "tags", _SWEEP))
    return cls(tags=tuple(SweptTagReport.from_dict(entry, row) for entry in tags))

SweptTagReport dataclass

SweptTagReport(tag: str, status: str, report: _SweepRow | None = None, kind: str | None = None, message: str | None = None)

One tag's outcome within a SweepReport (C-017).

sweep.rs:65-89. report, kind, and message are all absent-when-none (D7) — #[serde(skip_serializing_if = "Option::is_none")] upstream, not null.

report is present on a failed row too, when a --signature-format both tag lands one leg and loses the other: hiding the leg that landed would leave an operator re-signing what is already published.

ATTRIBUTE DESCRIPTION
tag

The tag this row describes.

TYPE: str

status

"completed", "skipped", or "failed", carried as raw str (D8).

TYPE: str

report

The tag's own report, when one was produced — a SignatureReport under a swept sign, an AttestationReport under a swept attest. Which one it is follows from which command called from_dict, not from anything in the row itself, which is why parsing it takes a row parser rather than guessing.

TYPE: _SweepRow | None

kind

The machine-branchable failure slug, when this tag failed.

TYPE: str | None

message

Sanitized prose describing the failure, free to be reworded — the same rule RefusedEntry.reason follows.

TYPE: str | None

from_dict classmethod

from_dict(data: Mapping[str, Any], row: Callable[[Mapping[str, Any]], _SweepRow]) -> SweptTagReport

Build from one decoded entry of SweepReport.tags.

PARAMETER DESCRIPTION
data

The decoded row.

TYPE: Mapping[str, Any]

row

Parses this row's report object — SignatureReport.from_dict for a swept sign, AttestationReport.from_dict for a swept attest. The one place this module's "no new parsing infrastructure" rule is deliberately relaxed (C-017): one struct serves both commands rather than duplicating it.

TYPE: Callable[[Mapping[str, Any]], _SweepRow]

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any], row: Callable[[Mapping[str, Any]], _SweepRow]) -> SweptTagReport:
    """Build from one decoded entry of `SweepReport.tags`.

    Args:
        data: The decoded row.
        row: Parses this row's `report` object —
            `SignatureReport.from_dict` for a swept `sign`,
            `AttestationReport.from_dict` for a swept `attest`. The one
            place this module's "no new parsing infrastructure" rule is
            deliberately relaxed (C-017): one struct serves both
            commands rather than duplicating it.
    """
    report = data.get("report")
    return cls(
        tag=_need(data, "tag", _SWEEP),
        status=_need(data, "status", _SWEEP),
        report=row(_table(report)) if report is not None else None,
        kind=data.get("kind"),
        message=data.get("message"),
    )

TestResult dataclass

TestResult(status: str, run: TestRun | None = None, assertion: Assertion | None = None)

ocx package test --script — the stable v1 envelope.

Only the --script form produces this. The trailing -- CMD form prints the child's raw stdout verbatim, even under --format json, so parsing that output here would either fail loudly or — worse — succeed on a child that happened to print JSON. The client layer never routes -- CMD output into this parser.

status decides pass or fail; assertion.kind says why a failure failed.

ATTRIBUTE DESCRIPTION
status

"passed" or the failure ocx named.

TYPE: str

run

What the tested command did, or None when nothing ran. Always-present-nullable (D7): script_run.rs:74 carries no skip_serializing_if, so a null here is ocx saying the script reached no terminal ocx.run — not a run whose fields defaulted.

TYPE: TestRun | None

assertion

The assertion that fired, or None when it passed. Always-present-nullable for the same reason (script_run.rs:73).

TYPE: Assertion | None

passed property

passed: bool

Whether the test passed — status decides, never the exit code.

from_json classmethod

from_json(raw: str) -> TestResult

Parse ocx --format json package test --script ... output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> TestResult:
    """Parse `ocx --format json package test --script ...` output."""
    data = _object(raw, "package test")
    assertion = _need(data, "assertion", "package test")
    run = _need(data, "run", "package test")
    return cls(
        status=_need(data, "status", "package test"),
        run=TestRun.from_dict(_table(run)) if run is not None else None,
        assertion=Assertion.from_dict(_table(assertion)) if assertion is not None else None,
    )

TestRun dataclass

TestRun(exit_code: int, stdout: str, stderr: str, duration_ms: int, truncated: bool)

What the tested command actually did.

ATTRIBUTE DESCRIPTION
exit_code

The command's exit status.

TYPE: int

stdout

Its stdout, truncated if truncated.

TYPE: str

stderr

Its stderr, same.

TYPE: str

duration_ms

Wall time in milliseconds.

TYPE: int

truncated

Output was cut to fit the envelope.

TYPE: bool

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> TestRun

Build from the decoded run object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> TestRun:
    """Build from the decoded `run` object."""
    return cls(
        exit_code=int(_need(data, "exit_code", "package test")),
        stdout=str(_need(data, "stdout", "package test")),
        stderr=str(_need(data, "stderr", "package test")),
        duration_ms=int(_need(data, "duration_ms", "package test")),
        truncated=bool(_need(data, "truncated", "package test")),
    )

ToolBinding dataclass

ToolBinding(declared: str | None = None, platforms: Mapping[str, str] | None = None)

One tool in a group of ocx status, and how far it is bound.

Binding state is key presence, not a status field: declared without platforms is declared-but-unlocked, platforms without declared is a stale lock entry nothing declares any more, and both means fully bound.

ATTRIBUTE DESCRIPTION
declared

The identifier ocx.toml declares, if it declares one.

TYPE: str | None

platforms

Platform to digest, from the lock file, if it has an entry.

TYPE: Mapping[str, str] | None

ref property

ref: PackageRef | None

The declared identifier as a PackageRef, or None if nothing declares it.

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> ToolBinding

Build from one decoded entry of a group's tools object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> ToolBinding:
    """Build from one decoded entry of a group's `tools` object."""
    return cls(declared=data.get("declared"), platforms=_opt_table(data.get("platforms")))

ToolRow dataclass

ToolRow(binding: str, group: str, digest: str, platforms: Mapping[str, str] = _EMPTY)

One tool entry add, lock, update, or remove reports.

All four emit the same bare array — effectively "the lock entries that changed", not a per-command envelope. remove reports an empty one.

ATTRIBUTE DESCRIPTION
binding

The binding name in ocx.toml.

TYPE: str

group

The group it belongs to.

TYPE: str

digest

The digest for the current platform.

TYPE: str

platforms

Every platform's digest.

TYPE: Mapping[str, str]

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> ToolRow

Build from one decoded array entry.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> ToolRow:
    """Build from one decoded array entry."""
    return cls(
        binding=_need(data, "binding", "lock"),
        group=_need(data, "group", "lock"),
        digest=_need(data, "digest", "lock"),
        platforms=_table(_need(data, "platforms", "lock")),
    )

VerificationReport dataclass

VerificationReport(subject_digest: str, referrer_digest: str, certificate_identity: str, certificate_oidc_issuer: str, signed_at: str, signatures: tuple[SignatureEntry, ...] = ())

ocx package verify (C-012).

Payload is enveloped (D11): from_json unwraps data.

ATTRIBUTE DESCRIPTION
subject_digest

The manifest digest that was verified.

TYPE: str

referrer_digest

The verified object's own digest. Not always a manifest digest — see SignatureEntry.referrer_digest.

TYPE: str

certificate_identity

The keyless identity pinned for verification. Plain String upstream (verification.rs:115) — always emitted, on both the keyless and key-based paths.

TYPE: str

certificate_oidc_issuer

The keyless OIDC issuer pinned for verification. Always emitted (verification.rs:117), same as above.

TYPE: str

signed_at

When the signature was produced, as ocx spelled it. Always emitted (verification.rs:119).

TYPE: str

signatures

Matching signatures found. Absent, not empty, when there were none (D7).

TYPE: tuple[SignatureEntry, ...]

from_json classmethod

from_json(raw: str) -> VerificationReport

Parse ocx --format json package verify output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> VerificationReport:
    """Parse `ocx --format json package verify` output."""
    data = _envelope(raw, _VERIFY)
    return cls(
        subject_digest=_need(data, "subject_digest", _VERIFY),
        referrer_digest=_need(data, "referrer_digest", _VERIFY),
        certificate_identity=_need(data, "certificate_identity", _VERIFY),
        certificate_oidc_issuer=_need(data, "certificate_oidc_issuer", _VERIFY),
        signed_at=_need(data, "signed_at", _VERIFY),
        signatures=tuple(SignatureEntry.from_dict(row) for row in _rows(data.get("signatures"))),
    )

VersionInfo dataclass

VersionInfo(version: str, channel: str | None = None, cargo_pkg_version: str | None = None, commit: Mapping[str, Any] = _EMPTY, build: Mapping[str, Any] = _EMPTY, ci: Mapping[str, Any] = _EMPTY)

ocx version under --format json.

Only version is guaranteed; everything else depends on how the binary was built. The three sub-objects stay mappings on purpose — they are build provenance, nothing in the SDK dispatches on their fields, and modelling them would pin a schema ocx explicitly keeps open for extension.

Note that Ocx.version() reads the plain output instead: a bare semver line is ocx's documented stable contract (design §11).

from_json classmethod

from_json(raw: str) -> VersionInfo

Parse ocx --format json version output.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_json(cls, raw: str) -> VersionInfo:
    """Parse `ocx --format json version` output."""
    data = _object(raw, "version")
    return cls(
        version=_need(data, "version", "version"),
        channel=data.get("channel"),
        cargo_pkg_version=data.get("cargo_pkg_version"),
        commit=_table(data.get("commit")),
        build=_table(data.get("build")),
        ci=_table(data.get("ci")),
    )

WhichResult dataclass

WhichResult(path: str, kind: str)

Where one identifier resolves on disk, per package which.

ATTRIBUTE DESCRIPTION
path

The resolved location.

TYPE: str

kind

"package" for a store directory, "shim" for a shim.

TYPE: str

from_dict classmethod

from_dict(data: Mapping[str, Any]) -> WhichResult

Build from one decoded value of the keyed object.

Source code in src/ocx_sdk/_results.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> WhichResult:
    """Build from one decoded value of the keyed object."""
    return cls(path=_need(data, "path", "package which"), kind=_need(data, "kind", "package which"))

BasicAuth dataclass

BasicAuth(user: str, password: str)

Username and password for a registry.

Example

BasicAuth("ci", "hunter2") BasicAuth(user='ci', password=*)

BearerAuth dataclass

BearerAuth(token: str)

A bearer token for a registry.

Example

BearerAuth("ghp_secret") BearerAuth(token=*)

Channel

Bases: StrEnum

Release channel of the dist manifest.

ConstVar dataclass

ConstVar(value: str)

An [env] value that replaces whatever the parent env had.

HostEnv dataclass

HostEnv(source: Mapping[str, str])

A snapshot of the environment a spawned ocx inherits.

The mapping is copied and made read-only at construction, so neither a later mutation of the source dict nor a write through .source can rewrite a snapshot that was already taken. (A read-only snapshot is not picklable — an implementation detail, not a contract.)

repr lists key names only. An ambient snapshot holds OCX_AUTH_* values, and a repr is exactly what a traceback or a pytest diff prints.

Example

HostEnv({"PATH": "/usr/bin", "TOKEN": "s"}).without("TOKEN").source["PATH"] '/usr/bin'

ambient classmethod

ambient() -> HostEnv

Return a snapshot of the current os.environ.

Source code in src/ocx_sdk/_types.py
@classmethod
def ambient(cls) -> HostEnv:
    """Return a snapshot of the current `os.environ`."""
    return cls(os.environ)

clean classmethod

clean() -> HostEnv

Return an empty environment.

Hermetic, and a documented footgun: spawned tools lose PATH and fail in ways that read like anything but a missing variable. minimal() is the recovery.

Source code in src/ocx_sdk/_types.py
@classmethod
def clean(cls) -> HostEnv:
    """Return an empty environment.

    Hermetic, and a documented footgun: spawned tools lose `PATH` and fail
    in ways that read like anything but a missing variable. `minimal()` is
    the recovery.
    """
    return cls({})

minimal classmethod

minimal(*, windows: bool | None = None) -> HostEnv

Return only the platform-essential variables from the ambient env.

PATH, HOME, and TMPDIR everywhere; SYSTEMROOT and TEMP on Windows. Variables that are not set are simply absent.

PARAMETER DESCRIPTION
windows

Force the platform branch. Defaults to detecting the host; tests pass it explicitly so both branches run anywhere.

TYPE: bool | None DEFAULT: None

Source code in src/ocx_sdk/_types.py
@classmethod
def minimal(cls, *, windows: bool | None = None) -> HostEnv:
    """Return only the platform-essential variables from the ambient env.

    `PATH`, `HOME`, and `TMPDIR` everywhere; `SYSTEMROOT` and `TEMP` on
    Windows. Variables that are not set are simply absent.

    Args:
        windows: Force the platform branch. Defaults to detecting the
            host; tests pass it explicitly so both branches run anywhere.
    """
    on_windows = os.name == "nt" if windows is None else windows
    extra = _MINIMAL_KEYS_WINDOWS if on_windows else ()
    return cls.ambient().only(*_MINIMAL_KEYS, *extra)

only

only(*keys: str) -> HostEnv

Return a snapshot narrowed to keys that are actually set.

Source code in src/ocx_sdk/_types.py
def only(self, *keys: str) -> HostEnv:
    """Return a snapshot narrowed to `keys` that are actually set."""
    return HostEnv({key: value for key, value in self.source.items() if key in keys})

without

without(*keys: str) -> HostEnv

Return a snapshot with keys removed.

Source code in src/ocx_sdk/_types.py
def without(self, *keys: str) -> HostEnv:
    """Return a snapshot with `keys` removed."""
    return HostEnv({key: value for key, value in self.source.items() if key not in keys})

InstallEnv

Bases: StrEnum

The OCX_INSTALL_* grammar the setup script defines.

bootstrap.ensure() honors these verbatim, one rung below its explicit keyword arguments and one above its own defaults.

ListVar dataclass

ListVar(value: str, *, separator: str | None = None)

An [env] value appended to a separated list.

ATTRIBUTE DESCRIPTION
value

The entry to append.

TYPE: str

separator

The list separator. None means the entry agrees with whatever separator the variable already uses; a conflict is an error at merge time, not a silent re-split.

TYPE: str | None

PackageRef dataclass

PackageRef(identifier: str, metadata: Mapping[str, object] = _NO_METADATA)

A package identifier carried verbatim from ocx, never parsed.

ocx owns the identifier grammar. The SDK stores the string it was given and hands it back unchanged, so a ref that came out of one result flows into the next call's argv byte-for-byte.

Identity is the identifier alone. metadata is decoration carried along from whichever JSON row produced the ref, so two refs to the same package from different commands must not compare unequal. The mapping is copied and made read-only at construction (and is therefore not picklable).

Example

ref = PackageRef("ocx.sh/astral-sh/uv:0.9.7") str(ref) 'ocx.sh/astral-sh/uv:0.9.7'

PathVar dataclass

PathVar(value: str)

An [env] value prepended to a PATH-like variable, deduplicated.

RetryPolicy dataclass

RetryPolicy(attempts: int = 3, backoff: float = 1.0, multiplier: float = 2.0, max_backoff: float = 30.0, max_retry_after: float = 300.0, jitter: bool = True, retry_on: frozenset[ExitCode] = frozenset({TEMP_FAIL}), sleep: Callable[[float], None] | None = None)

How to retry a failure ocx classified as transient.

Attempt one is not a retry: attempts=3 means at most two backoff waits. timeout is per attempt, so the worst case is attempts * timeout + sum(backoff).

ATTRIBUTE DESCRIPTION
attempts

Total attempts, including the first.

TYPE: int

backoff

Delay before the first retry, in seconds.

TYPE: float

multiplier

Growth factor applied to backoff per retry.

TYPE: float

max_backoff

Ceiling for the computed delay.

TYPE: float

max_retry_after

Ceiling for a server-supplied Retry-After. It is honored verbatim up to this value rather than truncated to max_backoff — truncating it would hammer a registry that just asked for room. Beyond it the answer is to fail, not to wait.

TYPE: float

jitter

Draw the delay uniformly from [0, delay] (AWS full jitter).

TYPE: bool

retry_on

Exit codes worth retrying. Applies to the process path only; _dist classifies by transport condition instead, so this set does not affect bootstrap.ensure().

TYPE: frozenset[ExitCode]

sleep

Sleep seam, defaulting to time.sleep. Async drivers use asyncio.sleep and ignore this field.

TYPE: Callable[[float], None] | None

DistSource dataclass

DistSource(manifest_url: str | None = None, manifest_path: Path | None = None, manifest_data: bytes | None = None, sha256: str | None = None, auth: Auth | None = None, headers: Mapping[str, str] = _NO_HEADERS)

Where a dist manifest comes from, and how its body is trusted.

Build one with url(), path(), or data() rather than the constructor. A source with no location at all is the canonical default: it resolves to OCX_INSTALL_DIST_URL when the environment sets one, and to DEFAULT_DIST_URL otherwise. A source built with an explicit URL never consults the environment.

ATTRIBUTE DESCRIPTION
manifest_url

Explicit manifest URL, or None for the default source.

TYPE: str | None

manifest_path

Local manifest file.

TYPE: Path | None

manifest_data

Manifest bytes the caller already holds.

TYPE: bytes | None

sha256

Expected digest of the manifest body. Auto-derived from a dist/<sha256>.json URL, and required for any URL source that is off-canonical or paired with a mirror.

TYPE: str | None

auth

Credentials for the manifest host, bound to its origin.

TYPE: Auth | None

headers

Extra request headers, bound to the same origin.

TYPE: Mapping[str, str]

Example

DistSource.url().manifest_url is None True

url classmethod

url(url: str | None = None, sha256: str | None = None, auth: Auth | None = None, headers: Mapping[str, str] | None = None) -> DistSource

Fetch the manifest over https.

PARAMETER DESCRIPTION
url

Manifest URL. None keeps the default source, which honors OCX_INSTALL_DIST_URL from the environment.

TYPE: str | None DEFAULT: None

sha256

Expected digest of the manifest body. Required whenever the host is not setup.ocx.sh or a mirror is in play; derived automatically from a dist/<sha256>.json URL.

TYPE: str | None DEFAULT: None

auth

Credentials for this origin, and this origin only.

TYPE: Auth | None DEFAULT: None

headers

Extra request headers, bound to the same origin.

TYPE: Mapping[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
DistSource

The source.

RAISES DESCRIPTION
DistManifestError

sha256 is not 64 lowercase hex digits.

Source code in src/ocx_sdk/_dist.py
@classmethod
def url(
    cls,
    url: str | None = None,
    sha256: str | None = None,
    auth: Auth | None = None,
    headers: Mapping[str, str] | None = None,
) -> DistSource:
    """Fetch the manifest over https.

    Args:
        url: Manifest URL. `None` keeps the default source, which honors
            `OCX_INSTALL_DIST_URL` from the environment.
        sha256: Expected digest of the manifest body. Required whenever the
            host is not `setup.ocx.sh` or a mirror is in play; derived
            automatically from a `dist/<sha256>.json` URL.
        auth: Credentials for this origin, and this origin only.
        headers: Extra request headers, bound to the same origin.

    Returns:
        The source.

    Raises:
        DistManifestError: `sha256` is not 64 lowercase hex digits.
    """
    return cls(manifest_url=url, sha256=sha256, auth=auth, headers=headers or _NO_HEADERS)

path classmethod

path(path: Path | str, sha256: str | None = None) -> DistSource

Read the manifest from a local file.

PARAMETER DESCRIPTION
path

File holding the manifest JSON.

TYPE: Path | str

sha256

Expected digest of the file contents.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
DistSource

The source.

RAISES DESCRIPTION
DistManifestError

sha256 is not 64 lowercase hex digits.

Source code in src/ocx_sdk/_dist.py
@classmethod
def path(cls, path: Path | str, sha256: str | None = None) -> DistSource:
    """Read the manifest from a local file.

    Args:
        path: File holding the manifest JSON.
        sha256: Expected digest of the file contents.

    Returns:
        The source.

    Raises:
        DistManifestError: `sha256` is not 64 lowercase hex digits.
    """
    return cls(manifest_path=Path(path), sha256=sha256)

data classmethod

data(raw: bytes, sha256: str | None = None) -> DistSource

Use manifest bytes the caller already has.

The vendoring path: ship a dist/<sha256>.json snapshot as package data and read it back with importlib.resources, which works from a zipped wheel where a filesystem path does not.

PARAMETER DESCRIPTION
raw

The manifest body.

TYPE: bytes

sha256

Expected digest of raw.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
DistSource

The source.

RAISES DESCRIPTION
DistManifestError

sha256 is not 64 lowercase hex digits.

Source code in src/ocx_sdk/_dist.py
@classmethod
def data(cls, raw: bytes, sha256: str | None = None) -> DistSource:
    """Use manifest bytes the caller already has.

    The vendoring path: ship a `dist/<sha256>.json` snapshot as package data
    and read it back with `importlib.resources`, which works from a zipped
    wheel where a filesystem path does not.

    Args:
        raw: The manifest body.
        sha256: Expected digest of `raw`.

    Returns:
        The source.

    Raises:
        DistManifestError: `sha256` is not 64 lowercase hex digits.
    """
    return cls(manifest_data=raw, sha256=sha256)

resolve_url

resolve_url(env: Mapping[str, str] | None = None) -> str | None

Return the URL this source fetches from.

PARAMETER DESCRIPTION
env

Environment consulted for OCX_INSTALL_DIST_URL; only a source without an explicit location reads it.

TYPE: Mapping[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
str | None

The manifest URL, or None for a local or in-memory source.

Source code in src/ocx_sdk/_dist.py
def resolve_url(self, env: Mapping[str, str] | None = None) -> str | None:
    """Return the URL this source fetches from.

    Args:
        env: Environment consulted for `OCX_INSTALL_DIST_URL`; only a
            source without an explicit location reads it.

    Returns:
        The manifest URL, or `None` for a local or in-memory source.
    """
    if self.manifest_path is not None or self.manifest_data is not None:
        return None
    if self.manifest_url is not None:
        return self.manifest_url
    return (env or {}).get(InstallEnv.DIST_URL) or DEFAULT_DIST_URL

credentials

credentials(env: Mapping[str, str] | None = None) -> Credentials

Return this source's credentials, bound to its resolved origin.

PARAMETER DESCRIPTION
env

Environment used to resolve the URL, as in resolve_url.

TYPE: Mapping[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
Credentials

Credentials whose origin is the resolved URL's, or an unbound empty

Credentials

set for a local or in-memory source.

RAISES DESCRIPTION
DistManifestError

The resolved URL is not usable — not https, or carrying userinfo.

Source code in src/ocx_sdk/_dist.py
def credentials(self, env: Mapping[str, str] | None = None) -> Credentials:
    """Return this source's credentials, bound to its resolved origin.

    Args:
        env: Environment used to resolve the URL, as in `resolve_url`.

    Returns:
        Credentials whose origin is the resolved URL's, or an unbound empty
        set for a local or in-memory source.

    Raises:
        DistManifestError: The resolved URL is not usable — not https, or
            carrying userinfo.
    """
    url = self.resolve_url(env)
    if url is None:
        return Credentials()
    return Credentials(validate_url(url, what="manifest URL"), self.auth, self.headers)

error_envelope

error_envelope(error: OcxProcessError) -> ErrorEnvelope | None

Return the structured failure a non-zero exit carried, else None.

The dual of partial_report: where that recovers the report a report-then-fail command wrote, this recovers the envelope a hard failure wrote instead. A failure never carries both, so for any one error at most one of the two answers.

try:
    ocx.package.claim("acme/widget", repository="oci://ghcr.io/acme/widget")
except DataError as exc:
    envelope = error_envelope(exc)
    if envelope is None or "already claimed" not in envelope.message:
        raise
PARAMETER DESCRIPTION
error

The caught process failure.

TYPE: OcxProcessError

RETURNS DESCRIPTION
ErrorEnvelope | None

None when stdout is empty, is not JSON, or is a report rather than

ErrorEnvelope | None

an envelope. Otherwise the parsed envelope.

RAISES DESCRIPTION
ValueError

stdout is an envelope missing one of its frozen keys — a binary outside the tested window, not a recoverable state.

Source code in src/ocx_sdk/_results.py
def error_envelope(error: OcxProcessError) -> ErrorEnvelope | None:
    """Return the structured failure a non-zero exit carried, else `None`.

    The dual of `partial_report`: where that recovers the *report* a
    report-then-fail command wrote, this recovers the *envelope* a hard
    failure wrote instead. A failure never carries both, so for any one
    error at most one of the two answers.

    ```python
    try:
        ocx.package.claim("acme/widget", repository="oci://ghcr.io/acme/widget")
    except DataError as exc:
        envelope = error_envelope(exc)
        if envelope is None or "already claimed" not in envelope.message:
            raise
    ```

    Args:
        error: The caught process failure.

    Returns:
        `None` when stdout is empty, is not JSON, or is a report rather than
        an envelope. Otherwise the parsed envelope.

    Raises:
        ValueError: stdout is an envelope missing one of its frozen keys —
            a binary outside the tested window, not a recoverable state.
    """
    try:
        payload = json.loads(error.stdout)
    except json.JSONDecodeError:
        return None
    if not _is_envelope(payload):
        return None
    return ErrorEnvelope.from_dict(payload)

partial_report

partial_report(error: OcxProcessError) -> str | None

Return the report a non-zero-exit stdout carried, else None (D10).

Several ocx failures still print a full JSON report before exiting non-zero: a --signature-format both sign or attest losing one leg, a partially-failed tag sweep, a copy refused on a sidecar conflict, and a push whose inline signing failed. OcxProcessError never parses its own stdout, so a caller recovers the report explicitly:

try:
    result = ocx.package.sign(ref, key="file://k.pem", rekor_upload=False)
except OcxProcessError as exc:
    raw = partial_report(exc)
    if raw is not None:
        result = SignatureReport.from_json(raw)
    else:
        raise
PARAMETER DESCRIPTION
error

The caught process failure.

TYPE: OcxProcessError

RETURNS DESCRIPTION
str | None

None for a hard failure (an error envelope carrying an error key

str | None

and no data, or stdout that is empty or unparseable as JSON) —

str | None

there is nothing to recover. Otherwise error.stdout verbatim, ready

str | None

for the matching from_json.

Note

The enveloped shapes come back enveloped, not pre-unwrapped. D10's text called for handing back the envelope's data sub-document, which cannot work: SignatureReport/AttestationReport/SweepReport's own from_json calls _envelope (D11), so a pre-unwrapped payload makes it unwrap twice and raise on a missing data key — the example above would not run. Returning stdout whole is what makes one from_json serve the success path and the recovery path, which is the property D10 and S-009 both ask for; the discriminator is unchanged.

Source code in src/ocx_sdk/_results.py
def partial_report(error: OcxProcessError) -> str | None:
    """Return the report a non-zero-exit stdout carried, else `None` (D10).

    Several ocx failures still print a full JSON report before exiting
    non-zero: a `--signature-format both` sign or attest losing one leg, a
    partially-failed tag sweep, a `copy` refused on a sidecar conflict, and a
    push whose inline signing failed. `OcxProcessError` never parses its own
    stdout, so a caller recovers the report explicitly:

    ```python
    try:
        result = ocx.package.sign(ref, key="file://k.pem", rekor_upload=False)
    except OcxProcessError as exc:
        raw = partial_report(exc)
        if raw is not None:
            result = SignatureReport.from_json(raw)
        else:
            raise
    ```

    Args:
        error: The caught process failure.

    Returns:
        `None` for a hard failure (an error envelope carrying an `error` key
        and no `data`, or stdout that is empty or unparseable as JSON) —
        there is nothing to recover. Otherwise `error.stdout` verbatim, ready
        for the matching `from_json`.

    Note:
        **The enveloped shapes come back enveloped**, not pre-unwrapped. D10's
        text called for handing back the envelope's `data` sub-document, which
        cannot work: `SignatureReport`/`AttestationReport`/`SweepReport`'s own
        `from_json` calls `_envelope` (D11), so a pre-unwrapped payload makes
        it unwrap twice and raise on a missing `data` key — the example above
        would not run. Returning stdout whole is what makes one `from_json`
        serve the success path and the recovery path, which is the property
        D10 and S-009 both ask for; the discriminator is unchanged.
    """
    try:
        payload = json.loads(error.stdout)
    except json.JSONDecodeError:
        return None
    if not _is_object(payload) or _is_envelope(payload):
        return None
    return error.stdout

tolerated_report

tolerated_report(result: CommandResult) -> str

Return the report a report-then-fail command wrote, or raise its failure.

package test, cascade check and cascade repair exit non-zero with their report on stdout when the outcome is a finding rather than a fault — the client tolerates that code (ok_codes) so the report comes back as a result. The same code with an error envelope, or with no JSON at all, is a fault the tolerance must not swallow: this raises it as the exception the exit code maps to, stdout preserved for error_envelope(err).

PARAMETER DESCRIPTION
result

The finished call, exit code and both streams.

TYPE: CommandResult

RETURNS DESCRIPTION
str

result.stdout, for the matching from_json.

RAISES DESCRIPTION
OcxProcessError

The exit was non-zero and stdout carried no report — the subclass _errors maps the code to, or the base class for a code ocx never assigns.

Source code in src/ocx_sdk/_results.py
def tolerated_report(result: CommandResult) -> str:
    """Return the report a report-then-fail command wrote, or raise its failure.

    `package test`, `cascade check` and `cascade repair` exit non-zero *with*
    their report on stdout when the outcome is a finding rather than a fault —
    the client tolerates that code (`ok_codes`) so the report comes back as a
    result. The same code with an error envelope, or with no JSON at all, is a
    fault the tolerance must not swallow: this raises it as the exception the
    exit code maps to, stdout preserved for `error_envelope(err)`.

    Args:
        result: The finished call, exit code and both streams.

    Returns:
        `result.stdout`, for the matching `from_json`.

    Raises:
        OcxProcessError: The exit was non-zero and stdout carried no report —
            the subclass `_errors` maps the code to, or the base class for a
            code ocx never assigns.
    """
    if result.exit_code == 0:
        return result.stdout
    try:
        payload = json.loads(result.stdout)
    except json.JSONDecodeError:
        raise _exit_error(result) from None
    if _is_envelope(payload):
        raise _exit_error(result)
    return result.stdout

ensure

ensure(version: str | None = None, *, channel: Channel = STABLE, dist: DistSource | None = None, mirror_url: str | None = None, ca_bundle: str | None = None, min_version: str | None = None, cache_dir: Path | None = None, env: HostEnv | None = None, trust_cache: bool = False, retry: RetryPolicy | None = None, timeout: float | None = None) -> Path

Provision a verified ocx binary and return its path.

Idempotent and offline-friendly: a cache hit that still hashes correctly needs no network at all. Every knob resolves explicit argument first, then the matching OCX_INSTALL_* variable from env, then the default.

PARAMETER DESCRIPTION
version

Exact version to install. None takes the channel's latest, then OCX_INSTALL_VERSION.

TYPE: str | None DEFAULT: None

channel

Channel consulted when version is None. No variable — the setup script has none either.

TYPE: Channel DEFAULT: STABLE

dist

Where the manifest comes from. None builds the default source, which honors OCX_INSTALL_DIST_URL; an explicitly constructed source does not.

TYPE: DistSource | None DEFAULT: None

mirror_url

Base URL that replaces the artifact host, as <mirror_url>/<tag>/<filename>, falling back to OCX_INSTALL_MIRROR_URL. The manifest digest is still enforced — a mirror relocates bytes, it never revalidates them.

TYPE: str | None DEFAULT: None

ca_bundle

PEM file trusted for every download, replacing the system trust store, falling back to OCX_INSTALL_CA_BUNDLE. For a TLS-intercepting proxy; the digest checks are unaffected.

TYPE: str | None DEFAULT: None

min_version

Operator floor. A resolved version below it fails loudly instead of installing something older than the caller allows.

TYPE: str | None DEFAULT: None

cache_dir

Cache root. None uses the per-user cache directory.

TYPE: Path | None DEFAULT: None

env

Environment snapshot. None reads the ambient one.

TYPE: HostEnv | None DEFAULT: None

trust_cache

Skip the digest re-check on a cache hit. Orthogonal to OCX_INSTALL_FORCE, which reinstalls even on a cache hit and has no argument of its own.

TYPE: bool DEFAULT: False

retry

Policy for transient transport failures; None tries once.

TYPE: RetryPolicy | None DEFAULT: None

timeout

Per-attempt network budget in seconds.

TYPE: float | None DEFAULT: None

RETURNS DESCRIPTION
Path

Path to the installed binary, mode 0o700.

RAISES DESCRIPTION
BootstrapError

The cache root is untrusted, or the resolved version is below min_version.

DistManifestError

The manifest is unusable, or its archive is.

UnsupportedPlatformError

No release matches this platform.

ChecksumMismatchError

Downloaded bytes do not match the manifest. Never retried — the bytes are wrong, not late.

DownloadError

The manifest or artifact could not be fetched, or the CA bundle could not be loaded.

Source code in src/ocx_sdk/_bootstrap.py
def ensure(
    version: str | None = None,
    *,
    channel: Channel = Channel.STABLE,
    dist: _dist.DistSource | None = None,
    mirror_url: str | None = None,
    ca_bundle: str | None = None,
    min_version: str | None = None,
    cache_dir: Path | None = None,
    env: HostEnv | None = None,
    trust_cache: bool = False,
    retry: RetryPolicy | None = None,
    timeout: float | None = None,
) -> Path:
    """Provision a verified ocx binary and return its path.

    Idempotent and offline-friendly: a cache hit that still hashes correctly
    needs no network at all. Every knob resolves explicit argument first, then
    the matching `OCX_INSTALL_*` variable from `env`, then the default.

    Args:
        version: Exact version to install. `None` takes the channel's latest,
            then `OCX_INSTALL_VERSION`.
        channel: Channel consulted when `version` is `None`. No variable — the
            setup script has none either.
        dist: Where the manifest comes from. `None` builds the default source,
            which honors `OCX_INSTALL_DIST_URL`; an explicitly constructed
            source does not.
        mirror_url: Base URL that replaces the artifact host, as
            `<mirror_url>/<tag>/<filename>`, falling back to
            `OCX_INSTALL_MIRROR_URL`. The manifest digest is still enforced —
            a mirror relocates bytes, it never revalidates them.
        ca_bundle: PEM file trusted for every download, replacing the system
            trust store, falling back to `OCX_INSTALL_CA_BUNDLE`. For a
            TLS-intercepting proxy; the digest checks are unaffected.
        min_version: Operator floor. A resolved version below it fails loudly
            instead of installing something older than the caller allows.
        cache_dir: Cache root. `None` uses the per-user cache directory.
        env: Environment snapshot. `None` reads the ambient one.
        trust_cache: Skip the digest re-check on a cache hit. Orthogonal to
            `OCX_INSTALL_FORCE`, which reinstalls even on a cache hit and has
            no argument of its own.
        retry: Policy for transient transport failures; `None` tries once.
        timeout: Per-attempt network budget in seconds.

    Returns:
        Path to the installed binary, mode `0o700`.

    Raises:
        BootstrapError: The cache root is untrusted, or the resolved version is
            below `min_version`.
        DistManifestError: The manifest is unusable, or its archive is.
        UnsupportedPlatformError: No release matches this platform.
        ChecksumMismatchError: Downloaded bytes do not match the manifest.
            Never retried — the bytes are wrong, not late.
        DownloadError: The manifest or artifact could not be fetched, or the
            CA bundle could not be loaded.
    """
    values = (env or HostEnv.ambient()).source
    wanted = version or values.get(InstallEnv.VERSION) or None
    mirror = mirror_url or values.get(InstallEnv.MIRROR_URL) or None
    bundle = ca_bundle or values.get(InstallEnv.CA_BUNDLE) or None
    opener = _dist.ca_bundle_opener(bundle) if bundle is not None else None
    source = dist or _dist.DistSource()
    system = platform.system()
    target = uname_to_triple(system, platform.machine(), rosetta=_translated_by_rosetta(system))
    root = Path(cache_dir or _default_cache_dir(values, system))
    _verify_root(root, posix=os.name != "nt")

    manifest = _dist.load_manifest(
        source, env=values, mirrored=mirror is not None, timeout=timeout, retry=retry, opener=opener
    )
    release = manifest.select(version=wanted, channel=channel, target=target)
    if min_version is not None and _version_key(release.version) < _version_key(min_version):
        raise BootstrapError(
            f"the manifest resolved ocx {release.version}, below the min_version={min_version} floor this caller "
            "requires; raise the floor or pin a newer version."
        )

    directory = root / release.version / release.target
    _secure_mkdir(directory, root)
    binary = directory / _binary_name(release.target)
    sidecar = binary.with_suffix(DIGEST_SUFFIX)
    if not _flag(values, InstallEnv.FORCE) and _cached(binary, sidecar, trust_cache=trust_cache):
        return binary
    _install(
        release,
        binary,
        sidecar,
        mirror_url=mirror,
        credentials=source.credentials(values),
        timeout=timeout,
        retry=retry,
        opener=opener,
    )
    return binary