- integrations
- Python
- Concepts
- Errors & credentials
Errors & credentials
The error model: exit code is the category
Section titled “The error model: exit code is the category”Every exception the SDK raises from a process failure derives from the exit
code alone — never from matching stderr text, which is exactly the kind of
heuristic that breaks quietly when a message’s wording changes upstream.
OcxError is the root; every
subclass’s __str__ carries an actionable next step, not just a fact,
because the message is what shows up in a CI log and a bare fact leaves the
reader guessing what to do about it.
from ocx_sdk import DataError
error = DataError(65, ["ocx", "add", "--bad-flag"], stderr="unexpected identifier")assert "next step" not in str(error) # every message names one; this just isn't it verbatimassert "stderr:" in str(error)Two catch shapes cover almost everything:
except OcxExecutionError— a process that failed and one that never finished (a non-zero exit and a timeout share this parent), the shape most callers actually want.except OcxProcessError— a non-zero exit specifically, with.exit_code(a plainint, because a signal-killed process exits with a code ocx never assigns —137forSIGKILL, say — and building an error object must never itself raise) and.retryable, which reports what ocx said about the failure (fixed:Trueonly for exit75), independent of whateverRetryPolicya caller happened to pass.
The full exit-code-to-exception table, and which codes retry by default,
lives on Environment & exit codes.
BootstrapError and its children (DownloadError,
ChecksumMismatchError, DistManifestError, UnsupportedPlatformError)
are a separate, non-process branch under OcxError — failures while
resolving, downloading, or installing a binary, before any typed command
ever runs.
The error envelope
Section titled “The error envelope”Since ocx 0.6.1 a hard failure that printed no report prints a structured
error envelope on stdout instead — {schema_version, command, exit_code, error: {kind, detail?, message, remediation?, context}}, a
contract frozen separately from the report schemas. The exit code is still
the category; the envelope is the detail, and
error_envelope is how a
caller reads it off a caught
OcxProcessError:
from ocx_sdk import DataError, error_envelope
error = DataError( 65, ["ocx", "package", "claim", "acme/widget"], stderr="", stdout='{"schema_version": 1, "command": "package claim", "exit_code": 65, ' '"error": {"kind": "data_error", "detail": "package_already_claimed", ' '"message": "package already claimed: p/acme/widget.json exists on main for acme/widget", "context": {}}}',)envelope = error_envelope(error)assert envelope is not Noneassert (envelope.kind, envelope.detail) == ("data_error", "package_already_claimed")None when stdout is empty, not JSON, or a report — the dual of
partial_report, which recovers the report a report-then-fail command
wrote; a failure never carries both. kind is the exit-code category’s
serde name; detail is the fine-grained variant slug, frozen by ocx so a
caller can branch on it — "package_already_claimed" is how an idempotent
CI step recognizes the claim it already made. Not every failure path
assigns one yet, so treat detail as optional; message is for humans and
never a branch.
Exit 86, ForgeCapabilityUnavailableError,
joins the table with 0.6.1: a forge write whose credential is valid but
whose target refuses job-token push. It is never retryable — the fix is an
administrator’s allowlist entry, not another attempt.
Verification makes install and pull fail where they used to pass
Section titled “Verification makes install and pull fail where they used to pass”ocx 0.6 verifies a package’s Sigstore signature before installing it,
whenever a [[trust.policy]] in the host’s config.toml covers that
package. This SDK’s 0.2.0 floor bump to ocx 0.6.0 is what puts that gate in
every caller’s path, so
package.install
and package.pull
can now raise where the identical call succeeded before — no code change on
your side, only a newer binary underneath. The failures are the ordinary
exit-code-mapped ones: DataError (65) for a signature that did not hold
up, PermissionDeniedError (77) for an identity or issuer the policy
refuses, NotFoundError (79) when a covered package carries no signature at
all, and TransparencyLogUnavailableError (83) when Rekor is unreachable.
None of them retry by default — the default retry_on is exit 75 alone —
and 83 in particular is deliberately excluded, because retrying a
transparency-log outage amplifies it for everyone else.
The trust policy is ambient host state: it lives in a config file the SDK does not own, read, or write, so the SDK cannot tell you in advance whether the gate applies to your packages. That is why this is documented rather than detected. To find out, run the install and catch the error.
The verify parameter on both methods is a three-state override —
True demands the check, False (--no-verify) skips it, None leaves
ocx’s own default, which is on. True is not enforcement: against a package
no policy covers there is nothing to verify against, and the flag is a
documented no-op rather than a promise that unsigned content will be
refused. False is the only way to skip verification through this SDK: the
ambient OCX_NO_VERIFY escape hatch is neutralized on every spawn, so an
opt-out has to be visible at the call site rather than in whatever exported
a variable three CI layers up.
Continues on Errors & credentials: PackageRef: carried, never parsed.