Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,682 changes: 1,614 additions & 68 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = [".", "crates/ironclaw_common", "crates/ironclaw_observability", "crates/ironclaw_host_api", "crates/ironclaw_filesystem", "crates/ironclaw_attachments", "crates/ironclaw_extractors", "crates/ironclaw_memory", "crates/ironclaw_memory_native", "crates/ironclaw_events", "crates/ironclaw_event_projections", "crates/ironclaw_event_streams", "crates/ironclaw_reborn_event_store", "crates/ironclaw_extensions", "crates/ironclaw_extension_host", "crates/ironclaw_processes", "crates/ironclaw_dispatcher", "crates/ironclaw_scripts", "crates/ironclaw_process_sandbox", "crates/ironclaw_mcp", "crates/ironclaw_wasm", "crates/ironclaw_wasm_limiter", "crates/ironclaw_capabilities", "crates/ironclaw_secrets", "crates/ironclaw_network", "crates/ironclaw_host_runtime", "crates/ironclaw_runtime_policy", "crates/ironclaw_authorization", "crates/ironclaw_run_state", "crates/ironclaw_approvals", "crates/ironclaw_resources", "crates/ironclaw_auth", "crates/ironclaw_trust", "crates/ironclaw_turns", "crates/ironclaw_agent_loop", "crates/ironclaw_threads", "crates/ironclaw_prompt_envelope", "crates/ironclaw_hooks", "crates/ironclaw_loop_host", "crates/ironclaw_runner", "crates/ironclaw_reborn_config", "crates/ironclaw_reborn_composition", "crates/ironclaw_reborn_identity", "crates/ironclaw_first_party_extensions", "crates/ironclaw_reborn_cli", "crates/ironclaw_reborn_traces", "crates/ironclaw_webui", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_conversations", "crates/ironclaw_product_adapters", "crates/ironclaw_product_context", "crates/ironclaw_product_workflow", "crates/ironclaw_product_adapter_registry", "crates/ironclaw_telegram_extension", "crates/ironclaw_telegram_v2_adapter", "crates/ironclaw_slack_extension", "crates/ironclaw_outbound", "crates/ironclaw_triggers", "crates/ironclaw_projects", "crates/ironclaw_architecture", "crates/ironclaw_safety", "crates/ironclaw_skills", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "tools/ironclaw_stress", "crates/ironclaw_attestation", "crates/ironclaw_signing_provider", "crates/ironclaw_wallet_external"]
members = [".", "crates/ironclaw_common", "crates/ironclaw_observability", "crates/ironclaw_host_api", "crates/ironclaw_filesystem", "crates/ironclaw_attachments", "crates/ironclaw_extractors", "crates/ironclaw_memory", "crates/ironclaw_memory_native", "crates/ironclaw_events", "crates/ironclaw_event_projections", "crates/ironclaw_event_streams", "crates/ironclaw_reborn_event_store", "crates/ironclaw_extensions", "crates/ironclaw_extension_host", "crates/ironclaw_processes", "crates/ironclaw_dispatcher", "crates/ironclaw_scripts", "crates/ironclaw_process_sandbox", "crates/ironclaw_mcp", "crates/ironclaw_wasm", "crates/ironclaw_wasm_limiter", "crates/ironclaw_capabilities", "crates/ironclaw_secrets", "crates/ironclaw_network", "crates/ironclaw_host_runtime", "crates/ironclaw_runtime_policy", "crates/ironclaw_authorization", "crates/ironclaw_run_state", "crates/ironclaw_approvals", "crates/ironclaw_resources", "crates/ironclaw_auth", "crates/ironclaw_trust", "crates/ironclaw_turns", "crates/ironclaw_agent_loop", "crates/ironclaw_threads", "crates/ironclaw_prompt_envelope", "crates/ironclaw_hooks", "crates/ironclaw_loop_host", "crates/ironclaw_runner", "crates/ironclaw_reborn_config", "crates/ironclaw_reborn_composition", "crates/ironclaw_reborn_identity", "crates/ironclaw_first_party_extensions", "crates/ironclaw_reborn_cli", "crates/ironclaw_reborn_traces", "crates/ironclaw_webui", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_conversations", "crates/ironclaw_product_adapters", "crates/ironclaw_product_context", "crates/ironclaw_product_workflow", "crates/ironclaw_product_adapter_registry", "crates/ironclaw_telegram_extension", "crates/ironclaw_telegram_v2_adapter", "crates/ironclaw_slack_extension", "crates/ironclaw_outbound", "crates/ironclaw_triggers", "crates/ironclaw_projects", "crates/ironclaw_architecture", "crates/ironclaw_safety", "crates/ironclaw_skills", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "tools/ironclaw_stress", "crates/ironclaw_attestation", "crates/ironclaw_signing_provider", "crates/ironclaw_wallet_external", "crates/ironclaw_chain_signing", "crates/ironclaw_attested_runtime"]
exclude = [
"channels-src/discord",
"channels-src/feishu",
Expand Down
151 changes: 140 additions & 11 deletions crates/ironclaw_architecture/tests/attested_signing_boundaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,15 @@ fn attestation_crate_has_no_chain_secrets_or_webauthn_dependency() {
);
}

/// The dependency names the external-wallet crate (PR7) must never carry. It
/// implements the browser injected provider and holds NO key material, so it
/// must not pull the heavy chain SDKs (`solana-sdk` / `near-primitives`) nor the
/// key-custody crate (`ironclaw_secrets`) nor the chain-signing crate
/// (`ironclaw_chain_signing`). It IS allowed `k256` / `ed25519-dalek` / `sha3` /
/// `sha2` for signer recovery + ed25519 verification, plus
/// `ironclaw_signing_provider` and `ironclaw_attestation` — so those are
/// deliberately absent from this list.
/// The dependency names the external-wallet crate (PR7 + PR9) must never carry.
/// It implements the browser injected provider (PR7) and the WalletConnect v2
/// provider (PR9) and holds NO key material, so it must not pull the heavy chain
/// SDKs (`solana-sdk` / `near-primitives`) nor the key-custody crate
/// (`ironclaw_secrets`) nor the chain-signing crate (`ironclaw_chain_signing`).
/// It IS allowed `k256` / `ed25519-dalek` / `sha3` / `sha2` for signer recovery
/// and ed25519 verification, the openssl-free WalletConnect relay fork
/// (`relay_client` / `relay_rpc`, PR9), plus `ironclaw_signing_provider` and
/// `ironclaw_attestation` — so those are deliberately absent from this list.
const WALLET_EXTERNAL_FORBIDDEN_DEPENDENCY_PREFIXES: &[&str] = &[
"solana-sdk",
"solana-program",
Expand Down Expand Up @@ -188,12 +189,85 @@ fn wallet_external_crate_has_no_chain_sdk_secrets_or_chain_signing_dependency()

assert!(
violations.is_empty(),
"ironclaw_wallet_external is the injected-wallet provider (PR7) and holds no key material; \
it must carry no chain SDK, secrets, or chain-signing dependency. Broadcasting the \
wallet-signed tx (ironclaw_chain_signing) is deferred to PR10. Forbidden dependencies \
"ironclaw_wallet_external is the external-wallet provider (PR7 + PR9) and holds no key \
material; it must carry no chain SDK, secrets, or chain-signing dependency. Broadcasting \
the wallet-signed tx (ironclaw_chain_signing) is deferred to PR10. Forbidden dependencies \
found:\n{}\nSee docs/plans/2026-05-23-attested-signing-substrate.md.",
violations.join("\n")
);

// Positive assertion (PR9): the openssl-free WalletConnect relay fork IS a
// dependency of the external-wallet crate. This guards against the WC deps
// being silently dropped (which would make the provider unbuildable) and
// documents that `relay_client` / `relay_rpc` are the allowed transport.
let names: Vec<&str> = dependencies
.iter()
.filter_map(|d| d["name"].as_str())
.collect();
for expected in ["relay_client", "relay_rpc"] {
assert!(
names.contains(&expected),
"ironclaw_wallet_external (PR9) must depend on `{expected}` from the openssl-free \
WalletConnect fork (tracecommons/walletconnect-rs). Present dependencies: {names:?}"
);
}
}

/// The whole attested-signing substrate is deliberately openssl-free: every TLS
/// path uses rustls/ring so the workspace carries no OpenSSL C dependency (no
/// system-openssl build/runtime coupling, smaller attack surface, reproducible
/// cross-compilation). PR9 adds the WalletConnect relay client via the
/// openssl-free fork (`tracecommons/walletconnect-rs`, rustls default,
/// `relay_rpc`'s `cacao` feature DISABLED — `cacao` pulls `alloy 0.3.6 → reqwest
/// default-tls → openssl`). This test fails if anything in the workspace graph
/// (re)introduces `openssl-sys`, against the Linux target where native-tls would
/// otherwise resolve to openssl.
///
/// If this regresses after a dependency change, the fix is to disable the
/// offending crate's openssl/native-tls feature and select rustls — NOT to
/// silence this test.
#[test]
fn workspace_graph_is_openssl_free() {
// `cargo tree -i <pkg>` exits non-zero with "did not match any packages"
// when the package is absent from the graph — exactly the success case here.
let manifest_path = workspace_root().join("Cargo.toml");
let output = Command::new("cargo")
.args([
"tree",
"--workspace",
"-i",
"openssl-sys",
"--target",
"x86_64-unknown-linux-gnu",
"--manifest-path",
])
.arg(&manifest_path)
.output()
.unwrap_or_else(|error| panic!("failed to run cargo tree: {error}"));

let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

if output.status.success() {
// A zero exit with non-empty output means openssl-sys IS in the graph.
let listed = stdout.trim();
assert!(
listed.is_empty(),
"the attested-signing workspace must stay openssl-free, but `openssl-sys` is in the \
dependency graph (Linux target):\n{listed}\n\nThe likely culprit is a native-tls / \
default-tls feature — most often `relay_rpc`'s `cacao` (alloy → reqwest default-tls). \
Disable it and select rustls; do NOT silence this test. See \
docs/plans/2026-05-23-attested-signing-substrate.md."
);
} else {
// Non-zero exit: confirm it is the "no such package" case (openssl-sys
// absent = success), not a cargo invocation failure.
assert!(
stderr.contains("did not match any packages"),
"cargo tree failed unexpectedly while checking for openssl-sys:\nstdout: {stdout}\n\
stderr: {stderr}"
);
}
}

fn cargo_metadata() -> Value {
Expand Down Expand Up @@ -225,3 +299,58 @@ fn workspace_root() -> std::path::PathBuf {
.expect("architecture crate must live under crates/ironclaw_architecture")
.to_path_buf()
}

/// `ironclaw_chain_signing` (PR6) is the ONE crate in the substrate that is
/// *allowed* to carry chain SDKs and secrets — it is the custodial signing
/// layer. This test is the inverse of the purity tests above: it asserts the
/// chain crate actually depends on at least one chain SDK and on
/// `ironclaw_secrets`, so a regression that accidentally moved chain/secret
/// code OUT of this crate (e.g. up into the pure attestation core) would be
/// caught from both directions.
#[test]
fn chain_signing_crate_carries_chain_sdk_and_secrets() {
let metadata = cargo_metadata();
let packages = metadata["packages"]
.as_array()
.expect("cargo metadata must include packages");

let package = packages
.iter()
.find(|package| package["name"] == "ironclaw_chain_signing")
.expect(
"ironclaw_chain_signing must be a workspace member; add it to the root \
Cargo.toml `workspace.members` (see attested-signing PR6)",
);

let dependencies = package["dependencies"]
.as_array()
.expect("package dependencies must be an array");
let dep_names: Vec<&str> = dependencies
.iter()
.filter_map(|d| d["name"].as_str())
.collect();

// It must depend on ironclaw_secrets (custodial keys are secrets).
assert!(
dep_names.contains(&"ironclaw_secrets"),
"ironclaw_chain_signing must depend on ironclaw_secrets (custodial keys are secrets); \
deps: {dep_names:?}"
);

// It must depend on at least one chain SDK (the whole point of the crate).
let chain_sdk_prefixes = ["alloy", "k256", "solana", "near-", "ed25519-dalek"];
assert!(
dep_names.iter().any(|name| chain_sdk_prefixes
.iter()
.any(|p| *name == *p || name.starts_with(p))),
"ironclaw_chain_signing must carry a chain SDK / signing primitive; deps: {dep_names:?}"
);

// And it must build on the lower substrate crates.
for required in ["ironclaw_signing_provider", "ironclaw_attestation"] {
assert!(
dep_names.contains(&required),
"ironclaw_chain_signing must depend on {required}; deps: {dep_names:?}"
);
}
}
5 changes: 1 addition & 4 deletions crates/ironclaw_attestation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
[package]
name = "ironclaw_attestation"
version = "0.1.0"
publish = false # internal substrate crate; not yet released to crates.io
edition = "2024"
rust-version = "1.92"
description = "Canonical signing-bytes + ApprovedTxHash core for the IronClaw attested-signing substrate (chain-SDK-free, no secrets/webauthn)"
authors = ["NEAR AI <support@near.ai>"]
license = "MIT OR Apache-2.0"
homepage = "https://github.com/nearai/ironclaw"
repository = "https://github.com/nearai/ironclaw"
# Internal workspace crate, never published to crates.io. Setting this lets the
# cargo-deny `bans` wildcard check pass without an allow-list entry (matches the
# `ironclaw_turns` convention — this is the PR where the crate is born).
publish = false

[package.metadata.dist]
dist = false
Expand Down
53 changes: 53 additions & 0 deletions crates/ironclaw_attested_runtime/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[package]
name = "ironclaw_attested_runtime"
version = "0.1.0"
edition = "2024"
rust-version = "1.92"
description = "Composition-layer runtime glue for the IronClaw attested-signing substrate: AttestedResumePort impl, signer-continuation driver, and the custodial-mainnet ship-gate (attested-signing PR10)"
authors = ["NEAR AI <support@near.ai>"]
license = "MIT OR Apache-2.0"
homepage = "https://github.com/nearai/ironclaw"
repository = "https://github.com/nearai/ironclaw"
publish = false

[package.metadata.dist]
dist = false

[dependencies]
async-trait = "0.1"
# Structured diagnostics for the broadcast-failure recovery path (a non-benign
# ledger error must surface in release builds, not be swallowed by debug_assert).
tracing = "0.1"

# Crypto-free turn-coordination contract (the AttestedResumePort trait lives here).
ironclaw_turns = { path = "../ironclaw_turns" }

# Attested-signing substrate stack (PR1-PR9).
ironclaw_signing_provider = { path = "../ironclaw_signing_provider" }
ironclaw_attestation = { path = "../ironclaw_attestation" }
ironclaw_chain_signing = { path = "../ironclaw_chain_signing" }
ironclaw_wallet_external = { path = "../ironclaw_wallet_external" }
# ResourceScope is the authoritative custodial keystore/AAD owner carried on the
# gate binding (host-api is already in the chain-signing dependency closure).
ironclaw_host_api = { path = "../ironclaw_host_api" }
# The driver reconstructs the EVM signable from the authoritative decoded
# binding (byte-drift defense), signing only the approved bytes.
alloy-consensus = "1"
alloy-primitives = "1"
alloy-eips = "1"

[dev-dependencies]
async-trait = "0.1"
chrono = "0.4"
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
hex = "0.4"
ed25519-dalek = "2"
# Already a normal dependency of `ironclaw_chain_signing` (in-tree); used here
# only to derive the EVM address from a fixed test private key, mirroring the
# `ironclaw_chain_signing` custodial tests. `evm::signing_key_from_bytes` became
# crate-private, so the test constructs the `SigningKey` directly. Adds no new
# transitive deps to the workspace tree.
k256 = { version = "0.13", features = ["ecdsa"] }
ironclaw_secrets = { path = "../ironclaw_secrets" }
ironclaw_host_api = { path = "../ironclaw_host_api" }
secrecy = "0.10"
Loading
Loading