Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8acc618
# manifest: add optional build provenance declaration
ualtinok Aug 26, 2026
1e359d6
control: define source-tagged supervisor provenance wire
iceteaSA Aug 24, 2026
73d2adc
# supervisor: retain spawn facts and probe executable identity
ualtinok Aug 26, 2026
cf2a4a3
supervisor: pin clearing and rescan-trap invariants in tests
iceteaSA Aug 24, 2026
a47b5d2
# control: relay declared and observed provenance without laundering
ualtinok Aug 26, 2026
28cecc2
provenance: align build identity on the shipped build_git_sha vocabulary
iceteaSA Aug 24, 2026
bc14646
provenance: assert the full declared-field class against daemon leakage
iceteaSA Aug 24, 2026
389f7ee
provenance: make the declared-field leakage sweep compile-exhaustive
iceteaSA Aug 24, 2026
b063e51
ck: add module provenance command
iceteaSA Aug 25, 2026
dfc7d74
ck: assert declared values never render under the observed label
iceteaSA Aug 25, 2026
ad55568
docs: document split provenance attestation
iceteaSA Aug 25, 2026
5433b85
manifest: bound declared provenance values and neutralize them at render
iceteaSA Aug 25, 2026
a5642da
provenance: cover daemon self-identity and reject empty declarations
iceteaSA Aug 25, 2026
0c95954
docs: state the declared-provenance refusal contract
iceteaSA Aug 25, 2026
da2db99
provenance: revert to SUBC_BUILD_GIT_SHA / SUBC_BUILD_LOCK_DIGEST
iceteaSA Aug 25, 2026
0d23898
test: assert the running-image verdict, not one platform's evidence s…
iceteaSA Aug 26, 2026
7cfeb6a
fix: gate the evidence import to the platforms that construct it
iceteaSA Aug 26, 2026
414cfad
twin: add SupervisorProvenance arm to dispatch-op discriminant (seman…
ualtinok Aug 26, 2026
3e3d984
twin: platform-gate provenance test imports (macOS + Windows clippy u…
ualtinok Aug 26, 2026
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
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/mcp-stdio-adapter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ fn manifest() -> ModuleManifest {
},
},
capabilities: None,
provenance: None,
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/subc-client-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "subc-client-rs"
version = "0.7.3"
version = "0.8.0"
edition = "2021"
publish = true
description = "Shared serve + consume client for Rust subc modules."
Expand All @@ -11,7 +11,7 @@ repository = "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/cortexkit/subconscious"
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
subc-control = { path = "../subc-control", version = "0.5" }
subc-control = { path = "../subc-control", version = "0.6" }
subc-protocol = { path = "../subc-protocol", version = "0.13" }
subc-transport = { path = "../subc-transport", version = "0.5" }
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }
Expand Down
1 change: 1 addition & 0 deletions crates/subc-client-rs/examples/echo-module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,6 @@ fn manifest(module_id: &str) -> subc_protocol::manifest::ModuleManifest {
},
},
capabilities: None,
provenance: None,
}
}
90 changes: 87 additions & 3 deletions crates/subc-client-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,31 @@ use subc_protocol::{
};
pub use subc_protocol::{
manifest::{
CapabilityDeclarations, CapabilityNeed, CapabilityRequirement, ExecutionMode, ProviderRole,
Tool,
CapabilityDeclarations, CapabilityNeed, CapabilityRequirement, ExecutionMode,
ManifestProvenance, ProviderRole, Tool,
},
session::{HealthReport, HealthStatus},
AdmissionClass,
AdmissionClass, SUBC_PROTOCOL_CRATE_VERSION,
};

pub fn build_provenance(
build_git_sha: Option<&str>,
build_lock_digest: Option<&str>,
store_schema_version: Option<&str>,
) -> ManifestProvenance {
ManifestProvenance {
build_git_sha: normalize_provenance_fact(build_git_sha),
build_lock_digest: normalize_provenance_fact(build_lock_digest),
wire_crate_version: Some(SUBC_PROTOCOL_CRATE_VERSION.to_string()),
store_schema_version: normalize_provenance_fact(store_schema_version),
}
}

fn normalize_provenance_fact(value: Option<&str>) -> Option<String> {
let value = value?.trim();
(!value.is_empty() && value != "unavailable").then(|| value.to_string())
}

use subc_transport::{
authenticate_client, connection_file, read_frame, write_frame, AuthError, ConnectionFileError,
FrameIoError,
Expand Down Expand Up @@ -1520,6 +1539,71 @@ mod tests {

use super::*;

#[test]
fn build_provenance_normalizes_clean_build_facts() {
let provenance = build_provenance(
Some(" 0123456789abcdef0123456789abcdef01234567 "),
Some(" lock-digest "),
Some(" schema-v3 "),
);

assert_eq!(
provenance,
ManifestProvenance {
build_git_sha: Some("0123456789abcdef0123456789abcdef01234567".to_string()),
build_lock_digest: Some("lock-digest".to_string()),
wire_crate_version: Some(SUBC_PROTOCOL_CRATE_VERSION.to_string()),
store_schema_version: Some("schema-v3".to_string()),
}
);
}

#[test]
fn build_provenance_preserves_a_dirty_revision_verbatim() {
let provenance = build_provenance(
Some("0123456789abcdef0123456789abcdef01234567-dirty"),
Some("lock-digest"),
None,
);

assert_eq!(
provenance.build_git_sha,
Some("0123456789abcdef0123456789abcdef01234567-dirty".to_string())
);
assert_eq!(
provenance.build_lock_digest,
Some("lock-digest".to_string())
);
}

#[test]
fn build_provenance_keeps_a_lock_digest_when_identity_is_unavailable() {
let provenance = build_provenance(Some("unavailable"), Some("lock-digest"), None);

assert_eq!(provenance.build_git_sha, None);
assert_eq!(
provenance.build_lock_digest,
Some("lock-digest".to_string())
);
assert_eq!(
provenance.wire_crate_version,
Some(SUBC_PROTOCOL_CRATE_VERSION.to_string())
);
}

#[test]
fn build_provenance_omits_fully_unavailable_inputs() {
let provenance = build_provenance(None, Some(" unavailable "), Some(" "));

assert_eq!(provenance.build_git_sha, None);
assert_eq!(provenance.build_lock_digest, None);
assert_eq!(provenance.store_schema_version, None);
assert_eq!(
provenance.wire_crate_version,
Some(SUBC_PROTOCOL_CRATE_VERSION.to_string())
);
}

struct EchoHandler;

#[async_trait]
Expand Down
1 change: 1 addition & 0 deletions crates/subc-client-rs/tests/real_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,7 @@ fn inline_module_manifest(module_id: &str, tool_names: &[&str]) -> ModuleManifes
},
},
capabilities: None,
provenance: None,
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/subc-control/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "subc-control"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
publish = true
description = "Client-facing subc control-plane wire shapes."
Expand Down
109 changes: 108 additions & 1 deletion crates/subc-control/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

#![forbid(unsafe_code)]

use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use subc_protocol::{
manifest::{CapabilityDeclarations, ProviderRole},
manifest::{CapabilityDeclarations, ManifestProvenance, ProviderRole},
session::HealthStatus,
BindIdentity, RouteTarget,
};
Expand Down Expand Up @@ -55,6 +57,7 @@ pub mod ops {
pub const SUPERVISOR_STDERR_TAIL: &str = "supervisor.stderr_tail";
pub const SUPERVISOR_TERMINALS: &str = "supervisor.terminals";
pub const SUPERVISOR_ROUTES: &str = "supervisor.routes";
pub const SUPERVISOR_PROVENANCE: &str = "supervisor.provenance";
}

/// Client-originated channel-0 control RPC body.
Expand Down Expand Up @@ -177,6 +180,13 @@ pub enum ClientControlRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<String>,
},
/// Report source-tagged provenance for supervised modules, optionally narrowed
/// to one module.
#[serde(rename = "supervisor.provenance")]
SupervisorProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<String>,
},
/// Retained stderr for one module.
///
/// A separate op rather than a field on `supervisor.list`: the tail is
Expand Down Expand Up @@ -287,6 +297,11 @@ pub enum ClientControlResponse {
},
#[serde(rename = "supervisor.routes")]
SupervisorRoutes { modules: Vec<SupervisorRouteModule> },
#[serde(rename = "supervisor.provenance")]
SupervisorProvenance {
daemon: SupervisorDaemonProvenance,
modules: Vec<SupervisorModuleProvenance>,
},
#[serde(rename = "supervisor.stderr_tail")]
SupervisorStderrTail {
module_id: String,
Expand Down Expand Up @@ -387,6 +402,98 @@ pub struct SupervisorRoute {
pub drain_reason: Option<RouteCloseReason>,
}

/// Source-tagged provenance for one supervised module.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SupervisorModuleProvenance {
pub module_id: String,
pub module_declared: ModuleDeclaredProvenance,
pub daemon_observed: SupervisorObservedProcess,
}

/// A module's declared build metadata, if its HELLO manifest carried it.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ModuleDeclaredProvenance {
Reported { build: ManifestProvenance },
Unverifiable,
}

/// Process facts observed by the daemon for a supervised module.
///
/// Build claims remain under `module_declared`; mixing them here would imply the
/// daemon independently observed module-provided metadata.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SupervisorObservedProcess {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spawned_at_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spawned_from: Option<PathBuf>,
pub running_image: RunningImageAgreement,
}

/// Daemon provenance paired with its runtime process observation.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SupervisorDaemonProvenance {
pub daemon_build: DaemonBuildProvenance,
pub daemon_observed: DaemonObservedProcess,
}

/// Build metadata embedded in the daemon binary.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DaemonBuildProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_git_sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_lock_digest: Option<String>,
}

/// Runtime process facts observed for the daemon itself.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DaemonObservedProcess {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at_ms: Option<u64>,
pub running_image: RunningImageAgreement,
}

/// Whether the executable currently running agrees with the spawned image.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum RunningImageAgreement {
Match {
evidence: RunningImageEvidence,
},
Mismatch {
running: RunningImageEvidence,
disk: RunningImageEvidence,
},
Unavailable {
reason: RunningImageUnavailableReason,
},
}

/// Platform-specific evidence used to compare a running image with its spawn path.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "method", rename_all = "snake_case")]
pub enum RunningImageEvidence {
LinuxProcSha256 { digest: String },
MacosSpawnInode { device: u64, inode: u64 },
}

/// Closed reasons why an executable identity could not be observed.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RunningImageUnavailableReason {
NotRunning,
UnsupportedPlatform,
RunningExecutableUnreadable,
SpawnedPathUnreadable,
HashFailed,
}

/// The identity tier the daemon can honestly report for a route consumer.
///
/// A caller that proved a live daemon-issued launch nonce is named `reserved`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"module_id": "aft",
"op": "supervisor.provenance"
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"supervisor.health",
"supervisor.stderr_tail",
"supervisor.terminals",
"supervisor.routes"
"supervisor.routes",
"supervisor.provenance"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"supervisor.health",
"supervisor.stderr_tail",
"supervisor.terminals",
"supervisor.routes"
"supervisor.routes",
"supervisor.provenance"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"supervisor.health",
"supervisor.stderr_tail",
"supervisor.terminals",
"supervisor.routes"
"supervisor.routes",
"supervisor.provenance"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"supervisor.health",
"supervisor.stderr_tail",
"supervisor.terminals",
"supervisor.routes"
"supervisor.routes",
"supervisor.provenance"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"supervisor.health",
"supervisor.stderr_tail",
"supervisor.terminals",
"supervisor.routes"
"supervisor.routes",
"supervisor.provenance"
]
}
Loading