polydeukes-sdk (Rust)
What this crate owns
Section titled “What this crate owns”Three blocking verbs execute a separately installed pdks: check_covenant, check_change_set,
and memory_search. The CLI owns judgment, configuration, and telemetry. The crate sends
caller-built inputs and returns results as values.
Every verb shares one resolver and process runner. A root becomes absolute relative to the caller’s
cwd and becomes the child’s cwd. An explicit executable is a file path relative to the caller’s
cwd, even for a bare filename, and preserves symlink spelling. Otherwise the first executable
regular file pdks in an absolute PATH directory is selected. Empty and relative entries are
skipped. Failure of the selected file never triggers another candidate.
Install
Section titled “Install”cargo add polydeukes-sdkInstall pdks for Linux x64, Linux arm64, or macOS arm64 using the
installation guide. For a Release download, extract the
.tar.gz archive first, then select the extracted executable through executable or an absolute
PATH directory. The crate runs that file directly and needs neither Node nor an npm installation.
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.
check_covenant
Section titled “check_covenant”use polydeukes_sdk::{ check_covenant, CheckCovenantSpec, CheckCovenantVerdict, CovenantInput, FileChange, ToolCall, Tools,};use std::{path::Path, time::Duration};
let input = CovenantInput { tool_calls: vec![ToolCall { name: "writeFile".into(), args: Default::default(), file_change: Some(FileChange::Create { path: "src/index.ts".into(), post: "export const answer = 42;\n".into(), }), }], subagent_spawns: vec![], user_messages: vec![], tools: Some(Tools { mutating: vec!["writeFile".into()], shell: vec!["exec".into()], command_args: vec!["command".into()], }), actor: None, session: None,};let verdict = check_covenant(CheckCovenantSpec { repo_root: Path::new("/path/to/project"), input: &input, enforce: None, config_layer: None, telemetry_path: None, executable: Some(Path::new("/opt/tools/pdks")), timeout: Some(Duration::from_secs(2)), spawn: None,});match verdict { CheckCovenantVerdict::Upheld { .. } => { /* Perform the proposed action here. */ } CheckCovenantVerdict::Blocked { reason } | CheckCovenantVerdict::Unjudged { reason } => { eprintln!("{reason}"); }}The host supplies each call’s evidence. FileChange::Create has path and post, Modify has
path, pre, and post, and Delete has path and optional pre. Missing evidence stays
missing; the SDK adds no actor, session, or tool roster. CovenantInput and its parts are generated
from core’s input schema. The CLI supplies world, which is absent from this input.
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.
The spec
Section titled “The spec”| Field | CheckCovenantSpec |
CheckChangeSetSpec |
|---|---|---|
repo_root: &Path |
Project root and child cwd | Same |
input: &CovenantInput |
Caller-built IR | Replaced by diff: &str |
enforce: Option<Enforce> |
None means Enforce::Block |
Same |
config_layer: Option<&Path> |
--config-layer; relative paths use child cwd |
Same |
telemetry_path: Option<&Path> |
--telemetry-path; relative paths use child cwd |
Same |
executable: Option<&Path> |
Explicit executable ahead of PATH |
Same |
timeout: Option<Duration> |
None means no deadline |
Same |
spawn |
Optional &dyn Fn(SpawnSpec) -> io::Result<SpawnOutcome> |
Same |
MemorySearchSpec takes root: &Path, executable, timeout, spawn, query: &str and
limit: Option<NonZeroU32>; None uses the CLI default. Memory commands receive no enforcement,
config-layer, or telemetry flags.
The timeout measures communication after spawn: writing stdin and draining both output pipes,
including descendant-held pipes after the direct child exits. Zero expires after spawn. Expiry kills
and reaps the direct child and ends pipe collection; descendants remain the host’s responsibility.
Resolution, serialization, and OS spawn delay precede the deadline. An injected spawn must enforce
its supplied timeout and return io::ErrorKind::TimedOut on expiry.
check_change_set
Section titled “check_change_set”check_change_set(CheckChangeSetSpec { ... }) takes diff in place of input and runs
pdks covenant check --diff --enforce <level>. It preserves UTF-8 diff text and newlines. The a/
and b/ paths are relative to repo_root. Produce a stable git format with:
git diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ HEADUntracked files need git add -N to appear. Both covenant verbs default to blocking enforcement,
including this change-set command. The crate does not run git.
The three verdicts
Section titled “The three verdicts”CheckCovenantVerdict has Upheld { advisories: String }, Blocked { reason: String }, and
Unjudged { reason: String }. Status 0 means Upheld, 2 means Blocked, and every other
status or signal means Unjudged. Resolution, serialization, execution, and timeout failures also
return Unjudged. Only Upheld authorizes the proposed action. Status 2 is not reclassified
using stderr.
Valid stderr is preserved with its whitespace; invalid UTF-8 uses replacement decoding. advisories
and completed nonzero responses expose stderr, with a generated status explanation where appropriate
when stderr is absent. Covenant stdout is drained without determining the verdict. The CLI writes
telemetry; telemetry_path selects its destination.
memory_search
Section titled “memory_search”use polydeukes_sdk::{memory_search, MemorySearchOutcome, MemorySearchSpec};use std::{num::NonZeroU32, path::Path};
let result = memory_search(MemorySearchSpec { root: Path::new("/path/to/project"), query: "session report", limit: NonZeroU32::new(5), executable: Some(Path::new("/opt/tools/pdks")), timeout: None, spawn: None,});match result { MemorySearchOutcome::Found { hits } => { /* Consume ranked hits. */ } MemorySearchOutcome::Empty => { /* Search succeeded with no matching sections. */ } MemorySearchOutcome::Unavailable { reason } => { /* Report the retrieval failure. */ }}memory_search sends the query as one literal argument to pdks memory search <query> --json and
adds --limit <n> when supplied. Memory search sends empty stdin.
The search output follows memory-search-output.schema.json in the repository’s schema/
directory. Search success requires strict UTF-8 JSON with a results array ({"results": [...]}).
Every hit needs string id, conceptId, docTitle, sectionTitle, status, boolean stale,
string-array supersededBy, trust (human-reviewed, machine-verified, unverified), and
matchPath (and, or, like). Extra object fields are accepted. Generated Rust fields use snake
case: concept_id, doc_title, section_title, match_path, superseded_by. Found { hits }
preserves CLI ranking and is nonempty. A valid empty results array alone means Empty; invalid
JSON, UTF-8, fields, enums, and execution failures mean Unavailable.
Process boundary and migration
Section titled “Process boundary and migration”Consumers that previously relied on npm discovery should install a standalone pdks and select it
via executable or an absolute PATH directory. Existing npm installs do not select the CLI.
Custom spawns now receive and return bytes rather than strings:
pub struct SpawnSpec { pub command: String, pub args: Vec<String>, pub cwd: std::path::PathBuf, pub stdin: Vec<u8>, pub timeout: Option<std::time::Duration>,}pub struct SpawnOutcome { pub status: Option<i32>, pub stdout: Vec<u8>, pub stderr: Vec<u8>,}None status means signal termination. Preserve output bytes until the SDK decodes them; converting
search stdout with lossy decoding would conceal invalid UTF-8. All three verbs use this same
boundary. Ordinary I/O errors become failure values; host termination and Rust panics are not
caught.
Declared limits
Section titled “Declared limits”The API blocks. An async host runs it in a blocking task. The crate does not negotiate the CLI version: select a CLI that understands its input schema and memory enums. Generated transport types cannot express every semantic constraint, such as a nonempty shell roster needing command argument names; the CLI checks those constraints. Memory retrieval is not action authorization.