Status: historical architecture note. The Rust local runtime has cut over; the older Rust parity prerequisite specs named by this document are archival context, not active release blockers.
This document captures the architectural decisions that the Rust parity specs depend on. The goal is to make the choices explicit once, so each spec can reference them rather than rederive them.
The Rust kernel started as conformance evidence for the TypeScript trusted
kernel, but the local runtime cutover is now the operating boundary. For trusted
local execution, Rust is the canonical owner once a command is advertised by
the native CLI or runtime. That includes graph execution, harness and dogfood
execution, receipt sealing and verification, policy and registry configuration,
authority admission, effect-family authority, sandbox admission/metadata, and local
built-in adapter execution plus external execution-adapter supervision. Future
OS sandbox enforcement belongs in runx-runtime, but current sandbox
declarations are not confinement by themselves.
TypeScript remains for generated contracts,
CLI/client wrappers, cloud/product integrations, host adapters, authoring
tooling, docs, conformance tests, and helper SDKs over language-neutral
protocols. It is not a runtime-local or adapters fallback for trusted local
behavior.
Rust ownership of orchestration does not require extension authors to write
Rust. External execution adapters target stable language-neutral protocols and
manifests; they must not require runx-runtime, runx-core, or a fork of the
core repo. Source ingress, hosted runtime binding, tool catalog/read-model, and
thread/outbox provider adapters are separate extension lanes, not implicit
members of the execution-adapter protocol.
Rust crates exist to:
- Prove behavioral parity through a shared fixture suite while a domain is still in the dual-tree window.
- Make TypeScript kernel drift explicit (intentional fixture refresh required).
- Provide the native local CLI/runtime path for skill, graph, harness, receipt, history, policy, authority, effect-family admission, sandbox admission/metadata, and external execution-adapter supervision.
Rust is not a second source of truth for cut-over surfaces; it is the source of truth. If Rust and TypeScript disagree on a fixture before a surface cuts over, the active cutover spec decides whether the fixture is still a TypeScript oracle or a Rust contract fixture.
MCP is native at the runtime boundary but not the kernel itself. The accepted
cross-repo decision is recorded in
../../docs/mcp-native-runtime-boundary.md: rmcp owns MCP protocol semantics
inside the adapter tier, while runx owns graph state, admission, authority,
approvals, pause/resume, receipts, and run identity.
"Pure kernel" in this document means exactly what the existing boundary check
enforces, not what the trusted-kernel doc lists. oss/scripts/check-boundaries.mjs
defines pureCoreDomains = ["parser", "policy", "state-machine"] and forbids
those domains from importing fs, child_process, http, net, and the
other node IO modules.
This document was written during the TypeScript-to-Rust kernel migration. The
trusted-kernel TypeScript paths it describes are now historical; runx-core
owns the active state-machine and policy behavior.
executor(369 lines)marketplaces(245 lines)parser(1658 lines)state-machine(667 lines)policy(retired from TypeScript and Rust-owned)
The plan ported state-machine + policy into Rust, which is 100% of
what pureCoreDomains enforces today. The remaining pure-by-imports domains
(executor, marketplaces, parser) are candidates for follow-up parity
specs; their boundary status would need to be added to pureCoreDomains
before a Rust port is meaningful.
The other five domains (artifacts, config, knowledge, receipts,
registry) use node modules and would need TS-side purification or a
clean split into pure-decision + impure-IO halves before they could move
to Rust.
So the scope of this plan is narrow but defensible: it covers exactly what the existing repo defines as pure. Future scope expansion is a sequence of explicit follow-up specs, not a vague "port the kernel" promise.
The future workspace under oss/crates/:
runx-contracts pure public contracts: CLI JSON, host protocol,
external execution-adapter protocol envelopes, receipts,
registry/tool records, act assignment
deps: serde, sha2
deferred deps: serde_json/thiserror only when concrete code
needs them outside tests
runx-core pure decisions: state-machine, policy, scope, sandbox normalization
deps: runx-contracts as needed, serde, thiserror as needed,
serde_json for private deterministic JSON canonicalization
posture: std default; no_std deferred to a follow-up spec
runx-parser pure: YAML -> AST -> intermediate representation
deps: runx-contracts, runx-core, serde, serde_norway,
serde_json, regex, thiserror
posture: public raw object subtrees use
`runx_contracts::JsonValue`; execution
semantics use `runx_contracts::execution`;
sandbox normalization uses `runx_core::policy`
runx-receipts pure: receipt model, hashing helpers, verification rules
deps: runx-contracts, serde, sha2
runx-sdk library: blocking CLI-backed SDK v0; future async path
deps: runx-contracts only in v0
explicit non-dep: runx-core in v0
runx-runtime impure: filesystem, subprocess, network, built-in adapter
execution, external execution-adapter supervision, MCP,
sandbox admission/metadata, and future OS enforcement
default features: none
opt-in features: cli-tool, mcp, mcp-http-server, a2a,
agent, catalog, external-adapter, http,
thread-outbox-provider
deps: runx-contracts, runx-core, runx-parser,
runx-receipts; async/network dependencies require
explicit adapter-tier exception specs
runx-cli binary: argument parsing, presentation, exit codes
includes: skill authoring subcommands until a separate
authoring library use case exists
deps: runx-runtime (long-term)
current: native local command surface for advertised Rust
commands; npm package is selector/client wrapper
Pure crates (runx-contracts, runx-core, runx-parser, runx-receipts,
and the CLI-backed v0 runx-sdk) depend only on each other when that coupling
is needed plus parsing, hashing, and serde-style support crates. runx-parser
depends on runx-contracts for JSON and execution-semantic boundary types and
on runx-core for sandbox normalization, so parser parity exercises the same
typed Rust surfaces the future runtime will consume.
runx-sdk is special: it is pure library code plus a blocking CLI client, but
it is not part of the trusted kernel and must not depend on runx-core in v0.
Crates below the runtime line own all side effects. The boundary between pure
and impure is enforced both by dependency direction and by cargo-deny/lint
rules (see section 10).
Parser parity uses serde_norway as the YAML backend. Raw object subtrees use
runx_contracts::JsonValue, and execution semantics are validated into the
runx_contracts::execution types so parser fixtures detect drift against the
contracts crate instead of carrying a duplicate local model.
Order of operations is committed, not loose. Pure crates ship first:
rust-contracts-bootstrap: crate graph, placeholder reservation versioning, andrunx-contractsplaceholder guardrails. This is the pre-kernel execution gate.runx-core(this plan): state-machine + policy parity.runx-contracts: public JSON/host/receipt contract parity for SDK/runtime. Follow-up spec.runx-parser: pure YAML/AST/IR parity. Implemented throughrust-parser-parityfor graphs, skills, runner manifests, tool manifests, and skill installs.runx-receipts: pure receipt model + verification rules. Follow-up spec.
Only after the initial pure set passes parity does any impure crate begin:
runx-sdkCLI-backed v0 can ship once its consumedrunx-contractssubset and CLI JSON cases are fixture-backed.runx-runtimeskeleton with one impure adapter execution path ported as a runtime feature (cli-tool first; MCP last because rmcp + tokio + sandbox + spawn semantics are the hardest cross-language surface).runx-clinative binary, gated byrust-cli-feature-parity-matrix.runx-sdknative-runtime feature, gated by the same runtime and CLI feature-parity evidence.
Each step is its own design pass. MCP cannot jump the queue.
runx-sdk has a special early path: a CLI-backed Rust SDK can ship before
runx-runtime exists, as long as it calls the authoritative runx binary and
only consumes documented JSON output from runx-contracts. That early SDK is
a blocking client wrapper and host-protocol type layer, not a native runtime.
Once runx-runtime exists, a separate spec may add the async SDK path. The
likely shape is runx-sdk exposing async APIs by default with a blocking
facade feature or sibling facade module, but that is not part of v0. SDK v0 is
allowed to block because it is only a subprocess-backed bridge to the current
CLI.
contracts-first-ordering: runx-contracts owns host protocol, external
execution-adapter protocol envelopes, capability execution, idempotency hashes,
and consumed JSON contract types before SDK Phase 2. runx-sdk may depend on
runx-contracts in CLI-backed v0, but it must not duplicate contract-owned
types or hash helpers.
There is no runx-authoring crate in the initial Rust shape. Skill authoring
helpers live in runx-cli subcommands or runx-sdk modules until there is a
clear library caller who needs authoring without either surface. The TypeScript
package split is useful history, not a forcing function for Cargo crates.
There is also no runx-adapters crate in the initial Rust shape. Built-in
adapter execution and external execution-adapter protocol supervision live under
runx-runtime feature flags
(cli-tool, mcp, mcp-http-server, a2a, agent, catalog,
external-adapter, http, thread-outbox-provider) until a family has an
independent publishing story;
a2a is contract-defined but not enabled in runx-cli. External execution adapter implementations live
outside the trusted core and talk to runx over language-neutral protocols. The
extension author's stable surface is a lane-specific manifest and wire contract,
plus optional helper SDKs; it is not a Rust crate dependency or a core fork.
There is no umbrella runx crate in the initial Rust shape. The runx crate
name is already taken by an unrelated crate, and the installable user-facing
surface is runx-cli with a binary named runx. If the name ever becomes
available or transferred, an umbrella crate can be proposed in a separate spec;
until then consumers depend on the specific crate they need.
The runtime crate is the impure owner, but it should not read as one flat bag of side effects. Current modules map to these implementation buckets:
- Service construction:
config,credentials,registry,runtime_http,receipts::paths,receipts::signing, and theRuntimeOptionsentrypoint. These modules resolve environment, credentials, registry roots, HTTP clients, receipt paths, and signing policy. They should become typed service facets that are constructed near runtime entrypoints, not leaf adapters. - Harness execution:
execution::harness,execution::orchestrator,execution::runner, andexecution::skill_front. These modules open the governed execution boundary, run graphs or skills, and seal receipts. - Adapter invocation:
adapter,adapters::*,agent_invocation,sandbox, andoutbox_provider. These modules resolve an invocation, admit it, run or supervise a process/protocol call, capture output, and return projected evidence to the harness. - Receipt and event projection:
receipts,journal,host,redaction, and the receipt tree verifier. These modules own durable evidence and the projections that feed history, hosted ingestion, and fixture oracles. - Authority algebra:
runx-core::policyplus runtime consumers inexecution::runner::authority,approval, effect-family adapters, andcredential_grant_policytests. Runtime code may record authority evidence, but checkable attenuation belongs in typed policy primitives. - CLI presentation:
runx-cliowns argument parsing, output, and exit codes. It may call runtime services, but should not recreate runtime semantics. - Dev and testing:
dev,doctor,scaffold,tool_catalogs, harness fixtures, adapter oracles, throughput scripts, and stress gates. These are product surfaces for authors and maintainers, not hidden fallback runtimes.
These buckets are not proposed Cargo crates. They are the internal ownership
map for runx-rust-runtime-architecture-lift-v1: split only where the split
creates a clearer dependency or capability boundary.
Current runtime lift shape:
runx-runtime::servicesowns small service facets for workspace environment, receipts, sandboxing, and adapter invocation. These facets are passed where they are needed; there is no catch-all harness context.runx-runtime::adapter_pipelineowns the shared adapter lifecycle: resolve, admit, invoke, capture, project, and seal. CLI-tool, external adapter, and MCP paths use the same lifecycle instead of parallel process stories.runx-runtime::lifecycleowns internal harness, decision, act, receipt, abnormal-seal, verification, and publication events before they project into local journal/history rows.runx_runtime::harness::list_cases()is the single Rust harness replay case registry. The oracle binary consumes that registry for check, regeneration, and JSON summary output.- Stress gates are explicit scripts (
stress:runtime:mcp,stress:runtime:cli-tool,stress:runtime:external-adapter,stress:runtime:fanout). They are not hidden inside the default fast loop.
runx-core is library-only and not published in this phase. Its public API is
shaped for two consumers:
- Fixture-runner tests (internal to this monorepo).
- Future internal consumers (
runx-parser,runx-receipts,runx-runtime,runx-cli).
Stability rules during the parity phase:
- The public surface is unstable. No SemVer guarantee. The crate version
stays
0.0.x. - Every module is
pub modonly if a fixture or sibling crate consumes it. Internal helpers stay private. - Re-exports at crate root follow the TypeScript export shape: one module per
TS sub-module (
state_machine,policy,policy::sandbox,policy::authority_proof,policy::public_work,policy::scope). - Naming preserves runx vocabulary.
admit_local_skillmatchesadmitLocalSkill. No invented aliases.
Publication to crates.io follows section 14.
TypeScript policy returns discriminated decision objects, for example
{ status: "approved", grant } or { status: "rejected", reason }.
Rust mirrors this shape with enums, not Result:
pub enum AdmissionDecision {
Approved(LocalAdmissionGrant),
Rejected { reason: AdmissionRejectionReason },
}Rationale:
Result<Grant, Reason>would imply rejection is exceptional. In policy code it is a normal, expected outcome.- Fixture JSON encodes both arms uniformly via the discriminator field.
- Callers can
matchexhaustively; new variants are breaking changes that surface at compile time.
Rejection reasons are typed enums (AdmissionRejectionReason,
SandboxRejectionReason, etc.), not free-form strings. The reason value in
fixtures uses the serde-renamed enum variant name.
Panics are forbidden in runx-core. The workspace Cargo.toml already denies
clippy::panic and clippy::unwrap_used for the launcher; the same lints
apply to all pure crates.
Fixture JSON is the cross-language contract. Conventions:
- All public types derive
serde::Serializeandserde::Deserialize. - Struct fields use
#[serde(rename_all = "camelCase")]to match TypeScript emit. This is the default for the whole crate. - Tagged unions use
#[serde(tag = "status")]or#[serde(tag = "kind")]matching the discriminator field name from TypeScript. Per-union choice is documented next to the type. - Enum variants without payloads use
#[serde(rename_all = "kebab-case")]to match TS string union values such as"in-progress","on-failure". - Optional fields use
Option<T>with#[serde(skip_serializing_if = "Option::is_none")]so fixture JSON matches TypeScript's omitted-key behavior. - No
#[serde(default)]on required fields. Missing fields are errors, not silently zero-valued. - Deduplicated arrays preserve first-seen insertion order from the TypeScript
oracle. Rust ports use
Vecplus insertion-preserving deduplication (IndexSetor equivalent) for serialized arrays such asrequestedScopesandgrantedScopes, notHashSetorBTreeSet.
A single crates/runx-core/src/serde_conventions.rs module documents these
rules in code comments and exposes a tiny test that round-trips a few golden
values, so the rules are not just prose.
The retired TypeScript policy module used Node path semantics for executable
normalization. Node's path module is OS-aware: on Windows it treats \ as a
separator, on POSIX it does not.
Decision: fixtures use POSIX semantics only. Executable names that contain
backslashes are normalized as if the separator were /, regardless of host
OS. Rationale:
- Fixtures are deterministic if and only if path semantics are platform-free.
- Cross-platform
node:pathbehavior produces different results for the same input string between Windows and POSIX runners; this would make fixtures host-dependent. - The Rust port implements its own
posix_basenamehelper rather than usingstd::path::Path, which is platform-aware.
If a real runtime consumer ever needs Windows-aware path handling, that lives
in runx-runtime, not in runx-core.
This also makes strict CLI-tool inline-code admission deterministic across
hosts. A command such as C:\Tools\node.exe normalizes to node everywhere,
so inline -e/--eval style invocations are denied consistently instead of
being bypassed on POSIX runners because backslashes were treated as ordinary
filename characters.
A TypeScript-side change is in scope for the fixtures spec: replace the
node:path import with a small posixBasename helper. This makes the kernel
truly side-effect-free (it currently imports a node-only module) and aligns
both languages on the same semantics. Flag for review.
runx-core uses std by default and does not gate no_std from day one.
Rationale:
- No concrete consumer needs
no_stdtoday. serde_jsonwithno_stdrequiresallocand a specific feature dance that adds friction to every dependency add. The kernel is small (<2,000 lines once ported); retrofittingno_stdlater if a real embedded or embedded-WASM consumer materializes is a half-day of work.- WASM works fine with
std. The hypothetical "in-browser preview" path does not requireno_std.
If a kernel-adjacent crate (runx-receipts for signing helpers in embedded
contexts, for example) ever ships a real no_std requirement, that decision
is revisited as a follow-up spec; it does not constrain this plan.
- Edition: 2024.
- Resolver: 3.
- MSRV: 1.85.0 (the first Rust release with edition 2024 support).
- MSRV pinned in
crates/Cargo.tomlworkspace[workspace.package]block and enforced in CI viarust-toolchain.tomlor an explicit toolchain in the workflow. - MSRV bumps are spec-level changes, not silent dependency updates.
The TypeScript boundary script (oss/scripts/check-boundaries.mjs)
forbids node APIs from policy and state-machine packages. The Rust side
needs the same discipline. Layered enforcement:
-
Dependency direction:
runx-core/Cargo.tomllists onlyrunx-contracts,serde,serde_json, and narrow test-only tools.serde_jsonis allowed for private deterministic JSON canonicalization, but public APIs exposerunx_contracts::JsonValue, notserde_json::Value.runx-parseralso exposes parser raw object subtrees asrunx_contracts::JsonValueand validates execution metadata into therunx_contracts::executiontypes. Notokio,reqwest,hyper,clap,rmcp. Enforced by dependency direction and by the workspacecargo-denydefault ban. The ban is intentionally stricter than "pure crates only" becausecargo-denyis workspace-scoped in this track; adapter/runtime-tier exceptions must be introduced by an explicit spec before the dependency is removed fromdeny.toml. -
API surface lint:
cargo-public-apisnapshots the public API. A diff against the snapshot in CI flags accidental surface growth. -
Forbidden imports: a lightweight build-time check (a
build.rsis overkill; a CI step runningcargo +nightly rustdoc -Z unstable-optionsor a simple grep over the crate's compiled deps tree) ensuresstd::process,std::fs,std::net,std::time::SystemTime,std::envare not referenced fromrunx-core/src. The grep is fragile but acceptable as a defense-in-depth signal alongside cargo-deny. -
Boundary check in TS: continues to apply. The existing
pnpm boundary:checkis the source of truth for TS-side enforcement; the Rust checks above mirror it on the Rust side.
cargo-deny configuration lives at oss/crates/deny.toml and is referenced
from CI.
Fixture-only testing covers known cases. For state-machine logic with several enums and fanout sync semantics, the long tail is large.
Plan:
- Phase 1 (fixtures spec): checked-in fixtures only. Enough to prove the contract works.
- Phase 2 (state-machine spec): add
propteststrategies for graph state transitions. Same generated inputs run through both languages via a TypeScript subprocess invoked from a Rust integration test, or vice versa. Differential failures pin a counterexample as a new fixture. - Phase 3 (policy spec): proptest strategies for admission and scope narrowing inputs.
Differential testing is optional in early phases but is the only realistic way to catch parity drift on combinatorial state spaces. Each parity spec declares whether it adopts it.
Once parity exists, the maintenance cost is real. The policy is staged:
- Phase A (advisory): Rust parity runs in CI through
scripts/check-rust-kernel-parity.mjs, but failure is a warning. TypeScript developers can break fixtures and regenerate them. A failing Rust check produces a CI annotation but does not block merge. The local command ispnpm rust:check. - Phase B (blocking): After 5 clean kernel-touching PRs land green in Phase A, Rust parity blocks merge. Every PR that touches retired state-machine or policy fixture behavior must either pass Rust parity or include an intentional fixture refresh (regenerated via the parity script).
Calendar time is not the trigger. If the kernel doesn't churn for weeks, the soak proves nothing; if it churns daily, calendar time is too coarse. PR count maps to actual exposure.
Expected cost after promotion: roughly +4 to +8 hours per kernel-touching PR for the dual-tree update (TS change, fixture regen, Rust port, Rust clippy/proptest update, public-API snapshot bump). Budget this explicitly; do not pretend it is free.
The transition between phases is a deliberate decision in the follow-up
rust-kernel-blocking-promotion spec, not automatic.
The dual tree is not the destination. The gating oracle for retiring
TypeScript implementations is rust-cli-feature-parity-matrix. Once that
matrix passes against a Rust runtime candidate, the cutover is triggered.
Sunset order (each step is its own cutover spec, not implicit):
- Replace TS state-machine consumers with
runx-core::state_machine. Completed; the TypeScript state-machine subtree is retired. - Replace TS policy consumers with
runx-core::policy. Completed; the TypeScript policy subtree is retired. - Port and delete
parser,executor,marketplaces(pure-by-imports trusted-kernel domains). - Port impure trusted-kernel domains (
artifacts,config,knowledge,receipts,registry) and delete the TypeScript runtime-local/adapters fallback. Each trusted local domain requires TS-side purification or a pure/impure split first. External adapters and plugins remain supported through their lane-specific language-neutral protocols, not through TypeScript executor internals. - Keep npm
@runxhq/clias a platform-aware selector that resolves and execs the Rust binary without requiring TypeScript source or tsx at runtime. - Move
runx-sdkfrom CLI-backed mode tonative-runtimeonce the runtime cutover is complete. Until then, the SDK remains a Rust client over the authoritative CLI and sharedrunx-contractstypes. TypeScript helper SDKs may remain as clients over CLI JSON, generated contracts, cloud HTTP, or ratified language-neutral protocol lanes.
Until step 1 is approved as its own spec, the dual tree is the operating state, and the cost in section 12 applies.
crates/runx-cli is now the native local command surface for the Rust-backed
commands it advertises. Help text is a contract: if a form appears in
runx --help, it must either execute through Rust or fail closed before any
hidden TypeScript fallback.
The npm @runxhq/cli package remains a platform-aware launcher/client wrapper,
but local execution semantics move through runx-runtime, not through
@runxhq/runtime-local or @runxhq/adapters. Installed package usage must be
useful without a checked-out TypeScript workspace. If the native binary is
missing or unsupported, the launcher fails closed with installation guidance;
it must not import a hidden TypeScript local runtime fallback.
New native command forms still require parity evidence:
fixtures/cli-parityexists and covers every current command, subcommand, flag, exit code, JSON output shape, human-output promise, receipt behavior, sandbox metadata path, adapter path, and documented workflow.runx-core,runx-parser, andrunx-receiptsexist and pass parity.- A
runx-runtimecrate exists with at least one impure adapter execution path ported. - A command-specific cutover or hardening spec proposes the move.
Kernel parity is not CLI parity. A Rust state-machine or policy port can prove that pure decisions match TypeScript, but it does not prove that the executable CLI is a drop-in replacement. Native CLI candidates must run against the fixture matrix and pass one-to-one feature parity before npm wrappers may present them as canonical.
The one-to-one CLI matrix belongs in fixtures/cli-parity/ and is governed by
the rust-cli-feature-parity-matrix spec. The matrix is intentionally broader
than kernel parity. It includes skill, evolve, resume, replay, diff,
search, add, inspect, history, knowledge show,
connect, config, new, init, harness, list, doctor, dev,
mcp serve, tool search, tool inspect, and tool build, plus any
pre-existing aliases that the matrix explicitly preserves and JSON/non-JSON
modes. export-receipts --trainable remains a TypeScript-maintained projection
command until a native export is explicitly promoted. New payment surfaces use
clean cutover names only: spend is the canonical consumer payment family,
while x402, stripe-spt, mpp, and mock are runtime paths behind that
family. Branded catalog skills such as x402-pay and stripe-pay may be public
adoption surfaces, but they must delegate to the same canonical spend flow and
seal the same spend receipt semantics. The localhost registry and checked-in
catalog now keep payment lifecycle internals as owned graph stages under
skills/spend/graph/*, skills/charge/graph/*, and skills/refund/graph/*.
Fixture and runtime-path packages remain internal wrappers that delegate to the
canonical graph skills; legacy payment-* aliases are not restored.
Cargo placeholders are also a crates.io name reservation strategy. The policy is explicit:
runx-clipublishes as the launcher package because it installs a usefulrunxbinary today. It is live at0.1.0.- Placeholder crates publish as explicit reservation releases at
0.0.1.runx-contracts,runx-receipts,runx-runtime, andrunx-sdkare live at0.0.1. runx-corewas reserved at0.0.1and now contains the first real Rust kernel surfaces: state-machine parity and policy parity. runx-core policy parity is not runtime-authoritative; it remains conformance evidence only until a cutover spec replaces TypeScript consumers.runx-parserwas reserved at0.0.1and now contains parser parity for the public TypeScript parser surfaces listed in its README. It remains conformance evidence until a TypeScript parser sunset spec replaces current consumers. It is markedpublish = false, depends on the localrunx-contractsandrunx-corecrates for shared boundary types, and its package check verifies those three crates together so Cargo does not resolve stale placeholder reservations from crates.io.- Placeholder README and crate docs must clearly say they are placeholders and do not provide native feature parity.
- Placeholder publishing is governed by
rust-placeholder-crates-publish. - The publish order must follow dependency direction:
runx-contracts,runx-parser,runx-receipts,runx-runtime,runx-sdk, withrunx-coreversioned independently as parity lands andrunx-cliindependent as the usable launcher package. - The first non-placeholder release of each crate requires its own fixture-backed parity spec.
The repo-root docs/trusted-kernel-package-truth.md remains the broad package
authority document for the full runx repository. The OSS workspace also keeps
oss/docs/trusted-kernel-package-truth.md as a Rust-parity addendum so scafld
specs executed from oss/ have a stable local docs path.
- Whether to adopt
bonor another builder crate for the larger value types. - Whether to expose a C ABI for non-Rust hosts (likely no, but not decided).
- Whether the Rust port targets a single big crate or splits state-machine and policy into their own crates from day one. Current decision: single crate. Split is a follow-up if the public surface grows too large.
- Whether a
runx-macroscrate is ever justified. There is no macros crate placeholder now. Procedural macros need a separate spec because they add build complexity and are easy to overuse.
crates/runx-core now implements state-machine and policy parity against the
checked-in fixture set. For still-dual consumers, those fixtures remain
conformance evidence. For advertised native runtime and CLI paths, Rust owns
the local policy, authority, and configuration decisions. crates/runx-contracts
now carries typed act-assignment and host-protocol contracts with parity
fixtures. Parser, receipt, registry, tool, runtime, SDK, and CLI surfaces move
from parity evidence to authority only through their named cutover specs.
External execution-adapter authors target language-neutral contracts and do not
need Rust or a core fork to add integrations. Non-execution extension lanes have
their own protocol contracts.
The Rust port must read like Rust, not like TypeScript mechanically translated into Rust syntax. For still-dual surfaces, the source of truth for behavior is the approved parity fixture or TypeScript oracle named by the active spec. For cut-over local surfaces, Rust is the behavioral source of truth. Rust's own API and style conventions own the implementation shape in either case. Fixture and wire parity are mandatory where the spec requires them; internal names, module boundaries, and helper structure should actively improve when the TypeScript shape is awkward or less idiomatic in Rust.
Code shape rules:
- Use Rust 2024 idioms:
let else,matches!,is_some_and,Option::then_some, exhaustivematch, and iterator combinators where they simplify control flow. Do not write clever iterator pipelines when a short loop is clearer. - Follow Rust API Guidelines naming: modules, functions, and values use
snake_case; types, traits, and enum variants useUpperCamelCase; error types use verb-object-error naming when an error type is needed. - Public APIs preserve runx vocabulary but not TypeScript casing:
admitLocalSkillbecomesadmit_local_skill, not a generated alias. - Prefer small value types, enums, and
matchover stringly typed records.serde_json::Valueis allowed in fixture tests, but not in the publicrunx-coreAPI. - Use
BTreeMapfor serialized maps whose key order reaches fixture JSON. Do not useHashMapinrunx-coreunless the value never crosses a serialization boundary and the spec explains why. - Keep helpers private by default. A function becomes
pubonly when a fixture runner or sibling crate needs it. Do not usepub use *. - Avoid macro-heavy abstractions.
deriveis fine; bespoke macros require a spec-level justification. - Avoid builder crates and fluent builders in
runx-coreunless a type has a real optional-field explosion. Plain structs and constructors are preferred. - Avoid clone-driven design. Small enums can derive
Copy; larger values are borrowed by slice/reference where straightforward. Cloning at fixture or serde boundaries is acceptable. - Keep modules scoped. A Rust source file above roughly 350 lines or a function above roughly 60 logical lines needs a short comment in the spec receipt explaining why it is still the clearest shape.
Error and failure rules:
- No
unsafe,panic!,todo!,unimplemented!,dbg!,unwrap, orexpectinrunx-coreproduction code. - No
anyhow,eyre,Box<dyn Error>, or dynamic error erasure inrunx-corepublic APIs. Policy decisions are normal enum values, not errors. Actual validation errors use concrete enums or structs, withthiserrorpreferred when derivingDisplayandstd::error::Errorkeeps the implementation smaller and clearer. - Tests may return
Resultand use?; fixture loaders should not rely onunwraporexpect.
Async and blocking rules:
runx-contracts,runx-core,runx-parser, andrunx-receiptsdo not depend ontokio,async-trait, HTTP clients, or subprocess libraries.runx-runtimeowns process management, network IO, MCP, sandbox enforcement, built-in adapter execution, external execution-adapter supervision, and adapter concurrency. It may own an async runtime or MCP/HTTP protocol crate only after a spec records the security rationale and updatescrates/deny.toml; until then the workspace ban is deliberate.runx-runtimedefaults to no adapter features. Built-in protocol host families are opt-in:cli-tool,mcp,mcp-http-server,a2a,agent,catalog,external-adapter,http, andthread-outbox-provider;a2ais contract-defined but not enabled inrunx-cli.runx-sdkv0 is explicitly a blocking CLI-backed client and depends onrunx-contracts, notrunx-coreorrunx-runtime. A future async SDK path requires its own spec and contract fixtures.- TypeScript helper SDKs may exist outside the trusted runtime as clients over generated contracts, CLI JSON, cloud HTTP, or ratified language-neutral protocol lanes. They must not embed a trusted local executor.
runx-climay bridge into the async runtime once it is native, but must not bypassrunx-runtimeby calling pure crates directly for runtime behavior.
Workspace policy:
- Commit the single workspace lockfile at
crates/Cargo.lock. This workspace contains therunx-clibinary plus publishable library crates, so the lock file is part of reproducible CI. cargo-nextestis not required for placeholder crates. It becomes a good follow-up once the Rust workspace has enough tests for nextest to materially improve CI feedback.
Enforcement:
cargo fmt --all --checkis required.cargo clippy -p runx-core --all-targets -- -D warningsis required.scripts/check-rust-core-style.mjschecks repository-specific shape rules that Clippy does not know: no publicserde_json::Value, noHashMapinrunx-core/src, no wildcard re-exports, no dynamic error erasure, no macro definitions, and line-count warnings for oversized files/functions.scripts/check-rust-crate-graph.mjschecks crate membership, placeholder reservation versioning, and dependency direction. Dependency relaxation is a spec-level change.cargo-public-apisnapshots ensure "just make it pub" does not become the easy escape hatch.
This bar deliberately avoids clippy::pedantic as a global deny. High-signal
lints are required; style churn is not.