Skip to content

polydeukes-sdk (Rust)

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.

Terminal window
cargo add polydeukes-sdk

Install 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.

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.

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(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:

Terminal window
git diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ HEAD

Untracked 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.

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.

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.

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.

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.