polydeukes-sdk (Python)
Python 3.12+ synchronous client for pdks covenant check and pdks memory.
Installation and
three-verb example.
Contract
Section titled “Contract”Each verb takes one keyword-only frozen dataclass spec and returns a frozen result value. Paths
accept str | os.PathLike[str]. Generated TypedDict inputs keep the CLI’s camelCase wire fields.
py.typed is included; runtime dependencies are empty.
| Verb | Required spec fields | Result |
|---|---|---|
check_covenant(CheckCovenantSpec(...)) |
repo_root, input: CovenantInput |
Upheld(advisories), Blocked(reason), Unjudged(reason) |
check_change_set(CheckChangeSetSpec(...)) |
repo_root, diff: str |
The same CheckCovenantVerdict union |
memory_search(MemorySearchSpec(...)) |
root, query: str |
Found(hits), Empty(), Unavailable(reason) |
Every spec has executable=None, timeout=None, and spawn=None. The covenant specs also have
enforce='block', config_layer=None, and telemetry_path=None. Search adds limit=None; a
supplied limit must be a positive integer. enforce accepts 'advise' or 'block'. Use
typing.assert_never after a complete class-pattern match to check that every result variant is
handled. Frozen dataclasses are shallow: their nested input dictionaries remain caller-owned.
Executable and process
Section titled “Executable and process”All three verbs use one resolver and runner. A specified executable is a file path relative to the
caller’s cwd, even for a bare name; its symlink spelling is preserved. Otherwise the first
executable regular file named pdks in an absolute PATH directory runs. Empty and relative
entries are skipped. A selected executable’s failure returns a failure result.
The child cwd is the absolute repo_root or root, based on the caller’s cwd. Configuration and
telemetry paths are passed unchanged; the child resolves relative values from that root. The SDK
sends the caller’s IR as UTF-8 JSON (allow_nan=False) or the diff’s unchanged UTF-8 bytes. It
supplies no evidence of its own. Query text stays one argv item. Standard streams are piped,
descriptors are closed, and no shell evaluates the command.
| Verb | Arguments after executable | stdin |
|---|---|---|
check_covenant |
covenant check --enforce <level> --unattended |
JSON |
check_change_set |
covenant check --diff --enforce <level> |
Unified diff |
memory_search |
memory search <query> --json [--limit <n>] |
Empty bytes |
check_covenant runs covenant check --enforce <level> --unattended, followed by --config-layer
and --telemetry-path when given. There is no option to turn off --unattended. A token in
session.userMessages does not open the witness valve, and disciplines whose bodies read
userTexts record skipped with reason no-observation for each call inside their scope. The SDK
requires pdks from the same release or later; an older pdks rejects --unattended with its
usage line and exit 2, which the SDK returns as a blocked verdict.
Both covenant verbs append --config-layer and --telemetry-path when specified. Generate a diff
with git diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ HEAD; untracked files
require git add -N first. The SDK does not run git.
timeout is a finite nonnegative number of seconds; 0 is an immediate deadline and None waits.
It starts after process creation, during communication. Timeout kills and reaps the direct child;
descendants keep running, and inherited pipes do not extend the deadline on Linux x64 and macOS
arm64. Serialization and executable resolution precede this deadline. An async caller uses
await asyncio.to_thread(check_covenant, spec).
Outcomes
Section titled “Outcomes”Covenant exit 0 gives Upheld; exit 2 gives Blocked; other statuses, signals, resolution,
serialization, spawn, and timeout failures give Unjudged. The child’s stderr is preserved in
advisories or reason. Exit 2 also includes CLI configuration failures: status alone is the
result contract. Read telemetry to identify the disciplines actually judged. Unjudged is not
permission to execute.
Memory search runs pdks memory ..., which invokes pdks-memory on the SDK process’s PATH.
Follow the installation guide to install the extension. For a local npm
installation, add the project’s absolute node_modules/.bin directory to PATH before starting the
SDK host. If it is missing, search returns Unavailable with the reason pdks prints.
The search output follows memory-search-output.schema.json in the repository’s schema/
directory. Memory nonzero exits, signals, spawn failures, and timeout give Unavailable. Search
requires valid UTF-8 JSON with a results array ({"results": [...]}). Every hit’s required
fields, types, and trust/matchPath enums are checked; additional fields are accepted. Only a
valid empty array gives Empty. Found.hits is a nonempty tuple that preserves CLI order; hit
fields retain camelCase. Malformed replies are Unavailable. Search retrieves sections; obligation
lookup is a separate CLI command.
Injecting spawn
Section titled “Injecting spawn”The optional synchronous callable takes
SpawnSpec(command: str, args: tuple[str, ...], cwd: str, stdin: bytes, timeout: float | None) and
returns SpawnOutcome(status: int | None, stdout: bytes, stderr: bytes). A None status means a
signal. The injected callable enforces the supplied timeout itself. Ordinary exceptions map to the
verb’s failure result; KeyboardInterrupt and SystemExit propagate.
Development and release
Section titled “Development and release”From the repository root:
uv sync --project packages/sdk-python --lockeduv run --project packages/sdk-python --locked python -m unittest discover -s packages/sdk-python/testsuv run --project packages/sdk-python --locked python packages/sdk-python/scripts/generate_types.py --checkuv run --project packages/sdk-python --locked pyright --project packages/sdk-python/pyproject.tomluv run --project packages/sdk-python --locked ruff check packages/sdk-pythonuv build --project packages/sdk-pythonTypes are generated deterministically from core’s two JSON schemas with the locked development
generator. The wheel and sdist install without runtime dependencies. Release automation bumps
project.version with the repository, verifies tag/package versions, tests the Linux executable
artifact, and publishes to PyPI with OIDC after executable builds pass. Existing PyPI versions are
skipped on retries.
Before the first release, the owner must configure a PyPI pending trusted publisher for project
polydeukes-sdk, owner huskyhoochu, repository polydeukes, workflow publish.yml, environment
pypi, and confirm the matching GitHub environment. This is a first-release prerequisite;
repository wiring alone does not establish that these external settings exist.
For a Python-only publication at an existing release version, create an annotated
sdk-python-v<version> tag containing the SDK and dispatch publish.yml from that tag with
python_only=true. This path verifies that the other packages’ sources and manifests match
v<version>, downloads that release’s Linux executable and checks its digest, then runs the same
Python distribution checks and PyPI upload. The npm/crate and executable release jobs are skipped.
Normal unified releases continue to use v<version> with python_only=false.
Shared SDK contract
Section titled “Shared SDK contract”The TS, Rust, and Python SDKs share explicit-executable → absolute-PATH selection, three verbs, closed results, and communication-deadline semantics. TS uses promises and milliseconds; Rust uses synchronous functions and Duration; Python uses synchronous functions and seconds. Core’s two schemas supply the generated wire types, separately from search-response validation.
From the checkout, node scripts/verify-sdk-contract.mjs --executable <absolute-pdks> compares all
three SDKs’ fixture observations, real judgments, telemetry, and search order. See the
TS
contract
and Rust contract.