Gate Idunn actuation with typed deployment brake

This commit is contained in:
Meta Vens 2026-07-19 23:56:42 +01:00
commit 2b1f1ee64e
5 changed files with 1369 additions and 784 deletions

View file

@ -54,6 +54,15 @@ Agents do not deploy daemons directly. They configure Idunn's target catalog,
release targets, migration commands, and command boundaries so Idunn can run
the shared rollout primitive and leave typed witnesses behind.
Deployment consequence has one final owner: immediately before every migration,
deploy, or restart process spawn, Idunn re-opens the root-owned deployment-brake
CultCache and the dedicated operator public anchor. A released record must bind
the canonical runtime (`yggdrasil` for `yggdrasil-local`), exact source revision
(or `restart:<daemon>`), exact request id, operator, signature, and short expiry.
Missing, corrupt, engaged, expired, foreign, wrong-scope, substituted, or
badly-signed state fails closed. Planning and lifecycle-command publication do
not bypass or cache this decision.
Local package surfaces:
- Organ contract: `docs/idunn.md`

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,12 @@ use cultcache_rs::{
CultCacheEnvelope, CultCacheExpectedEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore,
};
use cultnet_rs::{
GameCultProviderHealthIdentity, IdunnServiceIdentity, ServiceIdentityProfile,
ServiceIdentityTrustAnchor, derive_service_identity_id, enroll_service_identity_at,
GameCultProviderHealthIdentity, IDUNN_DEPLOYMENT_BRAKE_AUTHORITY, IDUNN_DEPLOYMENT_BRAKE_ID,
IDUNN_DEPLOYMENT_BRAKE_SCHEMA, IDUNN_DEPLOYMENT_BRAKE_SCOPE, IDUNN_DEPLOYMENT_RELEASE_PURPOSE,
IdunnDeploymentBrakeObservation, IdunnDeploymentBrakeOperatorIdentity,
IdunnDeploymentBrakeRecord, IdunnDeploymentBrakeReleasePurpose, IdunnServiceIdentity,
ServiceIdentityProfile, ServiceIdentityTrustAnchor, derive_service_identity_id,
enroll_service_identity_at, evaluate_idunn_deployment_brake,
export_service_identity_trust_anchor, open_service_identity_at,
};
use odin_core::{
@ -50,6 +54,26 @@ pub fn run(args: impl IntoIterator<Item = String>) -> Result<()> {
let signer = open_service_identity_at::<IdunnServiceIdentity>(&private)?;
export_service_identity_trust_anchor(&signer, &public)?;
}
"enroll-deployment-brake-operator" => {
require_only(&options, &["private-store"])?;
enroll_service_identity_at::<IdunnDeploymentBrakeOperatorIdentity>(&path(
&options,
"private-store",
)?)?;
}
"export-deployment-brake-operator-anchor" => {
require_only(&options, &["private-store", "public-anchor"])?;
let private = path(&options, "private-store")?;
let public = path(&options, "public-anchor")?;
reject_alias(&private, &public)?;
refuse_existing(&public, "deployment brake operator anchor")?;
let signer =
open_service_identity_at::<IdunnDeploymentBrakeOperatorIdentity>(&private)?;
export_service_identity_trust_anchor(&signer, &public)?;
}
"deployment-brake-engage" => deployment_brake_engage(&options)?,
"deployment-brake-release" => deployment_brake_release(&options)?,
"deployment-brake-status" => deployment_brake_status(&options)?,
"create-daemon-health-trust-binding" => create_health_binding(&options)?,
"add-daemon-health-trust-binding" => add_health_binding(&options)?,
"validate-daemon-health-trust-binding" => {
@ -63,6 +87,204 @@ pub fn run(args: impl IntoIterator<Item = String>) -> Result<()> {
Ok(())
}
fn deployment_brake_engage(options: &BTreeMap<String, String>) -> Result<()> {
require_only(
options,
&[
"store",
"runtime-id",
"owner",
"reason",
"observed-at-unix-millis",
],
)?;
let observed = parse_u64(options, "observed-at-unix-millis")?;
let record = IdunnDeploymentBrakeRecord {
schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(),
brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(),
runtime_id: required(options, "runtime-id")?.into(),
status: "engaged".into(),
scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(),
reason: required(options, "reason")?.into(),
observed_at_unix_millis: observed,
expires_at_unix_millis: None,
authorization_id: None,
authorization_purpose: None,
authorized_release_id: None,
authorized_deployment_id: None,
authorized_by: None,
authorization_issued_at_unix_millis: None,
authorization_expires_at_unix_millis: None,
signature_algorithm: None,
signature: None,
private_state_exposed: false,
updated_by: required(options, "owner")?.into(),
};
record.validate()?;
replace_brake(&path(options, "store")?, record, observed)
}
fn deployment_brake_release(options: &BTreeMap<String, String>) -> Result<()> {
require_only(
options,
&[
"store",
"private-store",
"runtime-id",
"owner",
"reason",
"authorization-id",
"release-id",
"deployment-id",
"issued-at-unix-millis",
"expires-at-unix-millis",
],
)?;
let issued = parse_u64(options, "issued-at-unix-millis")?;
let expires = parse_u64(options, "expires-at-unix-millis")?;
let signer = open_service_identity_at::<IdunnDeploymentBrakeOperatorIdentity>(&path(
options,
"private-store",
)?)?;
let mut record = IdunnDeploymentBrakeRecord {
schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(),
brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(),
runtime_id: required(options, "runtime-id")?.into(),
status: "released".into(),
scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(),
reason: required(options, "reason")?.into(),
observed_at_unix_millis: issued,
expires_at_unix_millis: Some(expires),
authorization_id: Some(required(options, "authorization-id")?.into()),
authorization_purpose: Some(IDUNN_DEPLOYMENT_RELEASE_PURPOSE.into()),
authorized_release_id: Some(required(options, "release-id")?.into()),
authorized_deployment_id: Some(required(options, "deployment-id")?.into()),
authorized_by: Some(signer.trust_anchor()?.identity_id),
authorization_issued_at_unix_millis: Some(issued),
authorization_expires_at_unix_millis: Some(expires),
signature_algorithm: Some("ed25519".into()),
signature: None,
private_state_exposed: false,
updated_by: required(options, "owner")?.into(),
};
record.signature = Some(
signer
.sign::<IdunnDeploymentBrakeReleasePurpose>(&rmp_serde::to_vec(&record)?)
.signature,
);
record.validate()?;
replace_brake(&path(options, "store")?, record, issued)
}
fn deployment_brake_status(options: &BTreeMap<String, String>) -> Result<()> {
allow_only(
options,
&[
"store",
"operator-anchor",
"runtime-id",
"release-id",
"deployment-id",
"now-unix-millis",
],
)?;
let record = read_brake(&path(options, "store")?)?;
if record.runtime_id != required(options, "runtime-id")? {
bail!("deployment brake belongs to another runtime")
}
if record.status == "engaged" {
println!(
"engaged runtime={} scope={} owner={} reason={}",
record.runtime_id, record.scope, record.updated_by, record.reason
);
return Ok(());
}
let anchor = read_operator_anchor(&path(options, "operator-anchor")?)?;
evaluate_idunn_deployment_brake(
IdunnDeploymentBrakeObservation::Present(&record),
&anchor,
required(options, "runtime-id")?,
required(options, "release-id")?,
required(options, "deployment-id")?,
parse_u64(options, "now-unix-millis")?,
)
.map_err(|d| anyhow!("deployment brake denied actuation: {d:?}"))?;
println!(
"released runtime={} release={} deployment={} owner={}",
record.runtime_id,
record.authorized_release_id.unwrap(),
record.authorized_deployment_id.unwrap(),
record.updated_by
);
Ok(())
}
fn replace_brake(path: &Path, record: IdunnDeploymentBrakeRecord, millis: u64) -> Result<()> {
let store = SingleFileMessagePackBackingStore::new(path);
let entries = store.pull_all_read_only_snapshot()?;
let current = match entries.as_slice() {
[] => None,
[entry]
if entry.r#type == IdunnDeploymentBrakeRecord::TYPE
&& entry.key == IDUNN_DEPLOYMENT_BRAKE_ID =>
{
Some(entry.clone())
}
_ => bail!("deployment brake store is corrupt or ambiguous"),
};
let next = typed_envelope(
IDUNN_DEPLOYMENT_BRAKE_ID,
&record,
IDUNN_DEPLOYMENT_BRAKE_SCHEMA,
millis,
)?;
if !store.compare_exchange(
&[CultCacheExpectedEnvelope {
r#type: IdunnDeploymentBrakeRecord::TYPE.into(),
key: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
current,
}],
&[next],
)? {
bail!("deployment brake changed during transition");
}
Ok(())
}
fn read_brake(path: &Path) -> Result<IdunnDeploymentBrakeRecord> {
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
let [entry] = entries.as_slice() else {
bail!("deployment brake is missing or ambiguous")
};
if entry.r#type != IdunnDeploymentBrakeRecord::TYPE
|| entry.key != IDUNN_DEPLOYMENT_BRAKE_ID
|| entry.schema_id.as_deref() != Some(IDUNN_DEPLOYMENT_BRAKE_SCHEMA)
{
bail!("deployment brake is foreign")
}
let record: IdunnDeploymentBrakeRecord = rmp_serde::from_slice(&entry.payload)?;
if rmp_serde::to_vec(&record)? != entry.payload {
bail!("deployment brake is noncanonical")
}
record.validate()?;
Ok(record)
}
fn read_operator_anchor(path: &Path) -> Result<ServiceIdentityTrustAnchor> {
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
let [entry] = entries.as_slice() else {
bail!("operator anchor is missing or ambiguous")
};
if entry.r#type != IdunnDeploymentBrakeOperatorIdentity::TRUST_ANCHOR_TYPE
|| entry.key != IdunnDeploymentBrakeOperatorIdentity::TRUST_ANCHOR_KEY
{
bail!("operator anchor is foreign")
}
Ok(rmp_serde::from_slice(&entry.payload)?)
}
fn validate_health_binding_store(path: &Path) -> Result<()> {
let entries = SingleFileMessagePackBackingStore::new(path).pull_all_read_only_snapshot()?;
if entries.is_empty() {
@ -395,6 +617,13 @@ fn require_only(options: &BTreeMap<String, String>, names: &[&str]) -> Result<()
Ok(())
}
fn allow_only(options: &BTreeMap<String, String>, names: &[&str]) -> Result<()> {
if let Some(name) = options.keys().find(|name| !names.contains(&name.as_str())) {
bail!("unsupported option --{name}");
}
Ok(())
}
fn required<'a>(options: &'a BTreeMap<String, String>, name: &str) -> Result<&'a str> {
options
.get(name)
@ -474,7 +703,7 @@ fn normalized(path: &Path) -> Result<PathBuf> {
}
fn usage() -> &'static str {
"Usage: idunn-provision enroll-idunn-identity --private-store <path>\n idunn-provision export-idunn-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision create-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision add-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision validate-daemon-health-trust-binding --input <path>\n idunn-provision create-provider-projection-trust-anchor --output <path> --trust-anchor-id <id> --runtime-id <id> --idunn-public-anchor <path> --bound-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision validate-provider-projection-trust-anchor --input <path> --idunn-public-anchor <path>"
"Usage: idunn-provision enroll-idunn-identity --private-store <path>\n idunn-provision export-idunn-public-anchor --private-store <path> --public-anchor <path>\n idunn-provision enroll-deployment-brake-operator --private-store <path>\n idunn-provision export-deployment-brake-operator-anchor --private-store <path> --public-anchor <path>\n idunn-provision deployment-brake-engage --store <path> --runtime-id <id> --owner <principal> --reason <text> --observed-at-unix-millis <u64>\n idunn-provision deployment-brake-release --store <path> --private-store <path> --runtime-id <id> --owner <principal> --reason <text> --authorization-id <id> --release-id <id> --deployment-id <id> --issued-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision deployment-brake-status --store <path> --operator-anchor <path> --runtime-id <id> [--release-id <id> --deployment-id <id> --now-unix-millis <u64>]\n idunn-provision create-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision add-daemon-health-trust-binding --output <path> --binding-id <id> --daemon <id> --health-contract <id> --source-runtime <id> --signer-public-key-hex <hex> --bound-at-unix-millis <u64> --release-binding-required <true|false>\n idunn-provision validate-daemon-health-trust-binding --input <path>\n idunn-provision create-provider-projection-trust-anchor --output <path> --trust-anchor-id <id> --runtime-id <id> --idunn-public-anchor <path> --bound-at-unix-millis <u64> --expires-at-unix-millis <u64>\n idunn-provision validate-provider-projection-trust-anchor --input <path> --idunn-public-anchor <path>"
}
#[cfg(test)]
@ -610,16 +839,45 @@ mod tests {
let temp = TempDir::new()?;
let store = temp.path().join("trust.cc");
let path = store.to_str().unwrap().to_string();
invoke(&binding_args("create-daemon-health-trust-binding", &path, "one", "daemon-one", "runtime-one"))?;
invoke(&binding_args(
"create-daemon-health-trust-binding",
&path,
"one",
"daemon-one",
"runtime-one",
))?;
let barrier = std::sync::Arc::new(std::sync::Barrier::new(3));
let workers = [("two", "daemon-two", "runtime-two"), ("three", "daemon-three", "runtime-three")].map(|(id, daemon, runtime)| {
let path = path.clone(); let barrier = barrier.clone();
std::thread::spawn(move || { barrier.wait(); invoke(&binding_args("add-daemon-health-trust-binding", &path, id, daemon, runtime)) })
let workers = [
("two", "daemon-two", "runtime-two"),
("three", "daemon-three", "runtime-three"),
]
.map(|(id, daemon, runtime)| {
let path = path.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
invoke(&binding_args(
"add-daemon-health-trust-binding",
&path,
id,
daemon,
runtime,
))
})
});
barrier.wait();
let wins = workers.into_iter().map(|worker| worker.join().unwrap()).filter(Result::is_ok).count();
let wins = workers
.into_iter()
.map(|worker| worker.join().unwrap())
.filter(Result::is_ok)
.count();
assert!((1..=2).contains(&wins));
assert_eq!(SingleFileMessagePackBackingStore::new(&store).pull_all_read_only_snapshot()?.len(), 1 + wins);
assert_eq!(
SingleFileMessagePackBackingStore::new(&store)
.pull_all_read_only_snapshot()?
.len(),
1 + wins
);
Ok(())
}
@ -877,4 +1135,148 @@ mod tests {
partial.extend(["--release-binding-required", "sometimes"]);
assert!(invoke(&partial).is_err());
}
#[test]
fn brake_operator_can_engage_then_issue_one_exact_bounded_release() -> Result<()> {
let temp = TempDir::new()?;
let private = temp.path().join("operator-private.cc");
let anchor = temp.path().join("operator-public.cc");
let store = temp.path().join("brake.cc");
invoke(&[
"enroll-deployment-brake-operator",
"--private-store",
private.to_str().unwrap(),
])?;
invoke(&[
"export-deployment-brake-operator-anchor",
"--private-store",
private.to_str().unwrap(),
"--public-anchor",
anchor.to_str().unwrap(),
])?;
invoke(&[
"deployment-brake-engage",
"--store",
store.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--owner",
"operator/metacrat",
"--reason",
"sleep mode",
"--observed-at-unix-millis",
"100",
])?;
assert_eq!(read_brake(&store)?.status, "engaged");
invoke(&[
"deployment-brake-release",
"--store",
store.to_str().unwrap(),
"--private-store",
private.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--owner",
"operator/metacrat",
"--reason",
"one deployment",
"--authorization-id",
"auth/r4",
"--release-id",
"commit-4",
"--deployment-id",
"request-4",
"--issued-at-unix-millis",
"200",
"--expires-at-unix-millis",
"800",
])?;
invoke(&[
"deployment-brake-status",
"--store",
store.to_str().unwrap(),
"--operator-anchor",
anchor.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--release-id",
"commit-4",
"--deployment-id",
"request-4",
"--now-unix-millis",
"500",
])?;
assert!(
invoke(&[
"deployment-brake-status",
"--store",
store.to_str().unwrap(),
"--operator-anchor",
anchor.to_str().unwrap(),
"--runtime-id",
"yggdrasil",
"--release-id",
"substituted",
"--deployment-id",
"request-4",
"--now-unix-millis",
"500"
])
.is_err()
);
Ok(())
}
#[test]
fn brake_compare_exchange_rejects_stale_transition_snapshot() -> Result<()> {
let temp = TempDir::new()?;
let store_path = temp.path().join("brake.cc");
let base = IdunnDeploymentBrakeRecord {
schema_version: IDUNN_DEPLOYMENT_BRAKE_SCHEMA.into(),
brake_id: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
authority: IDUNN_DEPLOYMENT_BRAKE_AUTHORITY.into(),
runtime_id: "yggdrasil".into(),
status: "engaged".into(),
scope: IDUNN_DEPLOYMENT_BRAKE_SCOPE.into(),
reason: "first".into(),
observed_at_unix_millis: 100,
expires_at_unix_millis: None,
authorization_id: None,
authorization_purpose: None,
authorized_release_id: None,
authorized_deployment_id: None,
authorized_by: None,
authorization_issued_at_unix_millis: None,
authorization_expires_at_unix_millis: None,
signature_algorithm: None,
signature: None,
private_state_exposed: false,
updated_by: "operator/a".into(),
};
replace_brake(&store_path, base.clone(), 100)?;
let stale = SingleFileMessagePackBackingStore::new(&store_path)
.pull_all_read_only_snapshot()?
.remove(0);
let mut changed = base.clone();
changed.reason = "racing writer".into();
changed.observed_at_unix_millis = 101;
replace_brake(&store_path, changed, 101)?;
let candidate = typed_envelope(
IDUNN_DEPLOYMENT_BRAKE_ID,
&base,
IDUNN_DEPLOYMENT_BRAKE_SCHEMA,
102,
)?;
assert!(
!SingleFileMessagePackBackingStore::new(&store_path).compare_exchange(
&[CultCacheExpectedEnvelope {
r#type: IdunnDeploymentBrakeRecord::TYPE.into(),
key: IDUNN_DEPLOYMENT_BRAKE_ID.into(),
current: Some(stale)
}],
&[candidate]
)?
);
Ok(())
}
}

View file

@ -10,7 +10,7 @@ User=idunn
Group=idunn
# Operational cancellation ceiling only. Target daemons own readiness through
# typed health; this duration must never be interpreted as deployment success.
ExecStart=/usr/local/bin/idunn --swarm-profile yggdrasil-local --repo-root /srv/odin/source --store /var/lib/gamecult/idunn/yggdrasil.keepalive.cc --release-authority-store /srv/bifrost/state/repository-release-authority.cc --rudp-health-bind 10.77.0.1:17870 --trusted-epiphany-health-identity-store /etc/gamecult/idunn/epiphany-health-identity.ccmp --command-timeout-seconds 21600 --execute
ExecStart=/usr/local/bin/idunn --swarm-profile yggdrasil-local --repo-root /srv/odin/source --store /var/lib/gamecult/idunn/yggdrasil.keepalive.cc --release-authority-store /srv/bifrost/state/repository-release-authority.cc --deployment-brake-store /var/lib/gamecult/idunn/deployment-brake.cc --deployment-brake-operator-anchor /etc/gamecult/idunn/deployment-brake-operator-anchor.cc --rudp-health-bind 10.77.0.1:17870 --trusted-epiphany-health-identity-store /etc/gamecult/idunn/epiphany-health-identity.ccmp --command-timeout-seconds 21600 --execute
Restart=always
RestartSec=5
TimeoutStopSec=20

2
vendor/cultnet-rs vendored

@ -1 +1 @@
Subproject commit a1001dc855aa4de04859d01d8be4c966f510f557
Subproject commit 402f6e7f0b938a251fc9443d734a6c86d5c1b40e