use std::time::Duration;
use serde::Deserialize;
const IDENTITY_SUBJECT: &str = "trusty-mpm";
const IDENTITY_PREDICATE: &str = "is_fact";
const IDENTITY_OBJECT: &str = "trusty-mpm (binary tm) is the Rust Meta-Harness / control plane, NOT the Python claude-mpm project; see crates/trusty-mpm/docs/WHAT-IS-TRUSTY-MPM.md or ~/.trusty-mpm/framework/docs/WHAT-IS-TRUSTY-MPM.md";
const IDENTITY_PROVENANCE: &str = "DOC-28 self-awareness seed";
#[derive(Debug, Deserialize)]
struct RawTriple {
#[serde(default)]
subject: String,
#[serde(default)]
predicate: String,
}
fn identity_fact_present(triples: &[RawTriple]) -> bool {
triples
.iter()
.any(|t| t.subject == IDENTITY_SUBJECT && t.predicate == IDENTITY_PREDICATE)
}
async fn seed_identity_prompt_fact(memory_url: &str, palace_id: &str) {
let client = match reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
{
Ok(c) => c,
Err(e) => {
tracing::warn!("identity-seed: could not build HTTP client: {e}");
return;
}
};
let base = memory_url.trim_end_matches('/');
let query_url = format!("{base}/api/v1/palaces/{palace_id}/kg");
let resp = match client
.get(&query_url)
.query(&[("subject", IDENTITY_SUBJECT)])
.send()
.await
{
Ok(r) => r,
Err(e) => {
tracing::warn!("identity-seed: could not reach trusty-memory at {memory_url}: {e}");
return;
}
};
if !resp.status().is_success() {
tracing::warn!(
"identity-seed: trusty-memory returned {} for identity kg_query",
resp.status()
);
return;
}
let triples: Vec<RawTriple> = match resp.json().await {
Ok(v) => v,
Err(e) => {
tracing::warn!("identity-seed: could not parse kg_query response: {e}");
return;
}
};
if identity_fact_present(&triples) {
tracing::debug!("identity-seed: identity prompt-fact already present, skipping assert");
return;
}
let assert_url = format!("{base}/api/v1/palaces/{palace_id}/kg");
let body = serde_json::json!({
"subject": IDENTITY_SUBJECT,
"predicate": IDENTITY_PREDICATE,
"object": IDENTITY_OBJECT,
"provenance": IDENTITY_PROVENANCE,
});
match client.post(&assert_url).json(&body).send().await {
Ok(r) if r.status().is_success() => {
tracing::info!(
"identity-seed: seeded trusty-mpm identity prompt-fact into palace {palace_id}"
);
}
Ok(r) => {
tracing::warn!(
"identity-seed: trusty-memory returned {} for identity kg_assert",
r.status()
);
}
Err(e) => {
tracing::warn!("identity-seed: could not reach trusty-memory at {memory_url}: {e}");
}
}
}
pub fn seed_identity_prompt_fact_blocking(memory_url: &str, palace_id: &str) {
let memory_url = memory_url.to_string();
let palace_id = palace_id.to_string();
let joined = std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
match rt {
Ok(rt) => rt.block_on(seed_identity_prompt_fact(&memory_url, &palace_id)),
Err(e) => {
tracing::warn!("identity-seed: could not build tokio runtime: {e}");
}
}
})
.join();
if joined.is_err() {
tracing::warn!("identity-seed: seed thread panicked; skipping");
}
}
pub(super) fn identity_seed_palace(
workspace_path: &std::path::Path,
repo_url: &str,
) -> Result<String, trusty_common::palace_resolve::PalaceResolveError> {
trusty_common::palace_resolve::resolve_palace_with_remote(workspace_path, Some(repo_url))
.map(|resolution| resolution.id)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_triple(subject: &str, predicate: &str) -> RawTriple {
RawTriple {
subject: subject.to_string(),
predicate: predicate.to_string(),
}
}
#[test]
fn identity_fact_present_detects_existing_fact() {
let triples = vec![
make_triple("some-other-subject", "is_fact"),
make_triple(IDENTITY_SUBJECT, IDENTITY_PREDICATE),
];
assert!(identity_fact_present(&triples));
}
#[test]
fn identity_fact_present_false_when_absent() {
let triples = vec![
make_triple(IDENTITY_SUBJECT, "some_other_predicate"),
make_triple("unrelated", IDENTITY_PREDICATE),
];
assert!(!identity_fact_present(&triples));
}
#[test]
fn identity_fact_present_false_for_empty_response() {
let triples: Vec<RawTriple> = vec![];
assert!(!identity_fact_present(&triples));
}
#[test]
fn identity_fact_present_skips_when_already_seeded() {
let already_seeded = vec![make_triple(IDENTITY_SUBJECT, IDENTITY_PREDICATE)];
assert!(
identity_fact_present(&already_seeded),
"a kg_query response containing the identity triple must be treated as already seeded"
);
}
#[tokio::test]
async fn seed_is_fail_open_on_unreachable_daemon() {
seed_identity_prompt_fact("http://127.0.0.1:1", "test-palace").await;
}
#[test]
fn seed_blocking_is_fail_open_on_unreachable_daemon() {
seed_identity_prompt_fact_blocking("http://127.0.0.1:1", "test-palace");
}
}