feat(helper): accept dormant mutation envelopes
Cut 2 adds a verify-envelope path beside the closure-token executor. The helper parses an envelope, binds it to the independently provisioned agent identity, validates time and lifetime bounds, selects the trusted key, verifies the signed contract, checks backend payload shape, and then refuses with backend_not_migrated. Every path emits an unsigned MutationReceipt. No backend executes, no replay slot is consumed, and no artifact-custody claim is made until the first existing backend migrates.
This commit is contained in:
parent
517f1aca20
commit
320ad46e00
5 changed files with 480 additions and 39 deletions
|
|
@ -28,6 +28,7 @@ use subtle::ConstantTimeEq;
|
|||
|
||||
#[allow(dead_code)]
|
||||
mod mutation_protocol;
|
||||
use mutation_protocol::{key_id_for, MutationEnvelope, MutationManifest, MutationOutcome, MutationReceipt};
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() {
|
||||
|
|
@ -60,6 +61,7 @@ const EXIT_MINT_STALE: i32 = 23; // gate evidence outside the freshness window
|
|||
const EXIT_MINT_KEY: i32 = 24; // mint key missing/unsafe permissions
|
||||
const EXIT_MINT_DUPLICATE: i32 = 25; // request_id already minted
|
||||
const EXIT_TRUST_PATH: i32 = 26; // trusted file/dir fails owner/perm/symlink validation (SEC-021)
|
||||
const EXIT_AUTHORIZATION_DENIED: i32 = 27; // envelope carries a non-allow authority decision
|
||||
|
||||
// Default on-host locations. All overridable by env so packaging/tests can relocate.
|
||||
const DEFAULT_KEYRING_DIR: &str = "/etc/redflag/trusted-keys";
|
||||
|
|
@ -230,38 +232,11 @@ fn local_agent_id() -> Result<String, Denial> {
|
|||
))
|
||||
}
|
||||
|
||||
fn key_id_for(pubkey: &[u8]) -> String {
|
||||
let digest = Sha256::digest(pubkey);
|
||||
hex::encode(&digest[..16])
|
||||
}
|
||||
|
||||
// Canonical UUID v4: 8-4-4-4-12 lowercase hex with version (4) and variant
|
||||
// (8/9/a/b) nibbles. Used to constrain request_id so the journal dedup
|
||||
// needle is always an exact-match target, never a substring of another ID.
|
||||
fn is_valid_uuid_v4(s: &str) -> bool {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() != 36 {
|
||||
return false;
|
||||
}
|
||||
// Hyphens at fixed positions
|
||||
if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
|
||||
return false;
|
||||
}
|
||||
// All non-hyphen positions must be lowercase hex digits
|
||||
for (i, &b) in bytes.iter().enumerate() {
|
||||
if i == 8 || i == 13 || i == 18 || i == 23 {
|
||||
continue;
|
||||
}
|
||||
if !b.is_ascii_hexdigit() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Version nibble at position 14 must be '4'
|
||||
if bytes[14] != b'4' {
|
||||
return false;
|
||||
}
|
||||
// Variant nibble at position 19 must be 8, 9, a, or b
|
||||
matches!(bytes[19], b'8' | b'9' | b'a' | b'b')
|
||||
mutation_protocol::is_canonical_uuid_v4(s)
|
||||
}
|
||||
|
||||
// SEC-021: the helper defends its own trust inputs instead of relying on the
|
||||
|
|
@ -633,14 +608,14 @@ fn execute_plan(plan: &[(String, Vec<String>)]) -> Result<(), Denial> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_result(result: &PolicyResult) {
|
||||
fn emit_result<T: Serialize>(result: &T) {
|
||||
match serde_json::to_string(result) {
|
||||
Ok(s) => println!("{}", s),
|
||||
Err(e) => log_error(&format!("result_serialize_failed error={}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_result_to_file(result: &PolicyResult, path: &str) {
|
||||
fn emit_result_to_file<T: Serialize>(result: &T, path: &str) {
|
||||
let json = match serde_json::to_string(result) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
|
|
@ -2103,6 +2078,427 @@ mod tests {
|
|||
req.operation = "remove".into();
|
||||
assert_eq!(run_mint(&req, &fx.paths, now).unwrap_err().code, EXIT_UNSUPPORTED_OP);
|
||||
}
|
||||
// ---- verify-envelope (ARCH-002 cut 2) ----
|
||||
// The envelope path refuses everything today. These pin the refusal order,
|
||||
// and that no refusal touches the replay ledger.
|
||||
|
||||
const TEST_TARGET: &str = "agent-123";
|
||||
const ENVELOPE_NOW: i64 = 1_700_000_100;
|
||||
|
||||
fn envelope_json(target: &str, authorization_id: &str, version: u32, expires_at: i64) -> String {
|
||||
format!(
|
||||
r#"{{"manifest":{{"protocol_version":{version},"operation_id":"550e8400-e29b-41d4-a716-446655440010","target_id":"{target}","backend":"pacman","operation":"install","resolved_actions":[{{"kind":"package","identity":"acl@2.3.2-1","payload":"{{\"repository\":\"core\"}}"}}],"evidence":[]}},"authorization":{{"protocol_version":{version},"authorization_id":"{authorization_id}","manifest_hash":"","authority_kind":"fleet-server","authority_id":"redflag-prod","target_id":"{target}","issued_at":1700000000,"not_before":1700000001,"expires_at":{expires_at},"decision":"allow","key_id":"00000000000000000000000000000000","signature":""}}}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn write_envelope(name: &str, body: &str) -> PathBuf {
|
||||
let dir = trust_fixture(name);
|
||||
let path = dir.join("envelope.json");
|
||||
fs::write(&path, body).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
const GOOD_AUTHORIZATION_ID: &str = "550e8400-e29b-41d4-a716-446655440011";
|
||||
|
||||
#[test]
|
||||
fn envelope_parse_failure_denies_without_identity() {
|
||||
let path = write_envelope("env-parse", "{ not json");
|
||||
let (envelope, denial) = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW);
|
||||
assert!(envelope.is_none());
|
||||
assert_eq!(denial.code, EXIT_BAD_TOKEN);
|
||||
assert_eq!(denial.reason, "envelope_parse_failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_unknown_version_fails_closed() {
|
||||
let path = write_envelope(
|
||||
"env-version",
|
||||
&envelope_json(TEST_TARGET, GOOD_AUTHORIZATION_ID, 2, 1_700_000_600),
|
||||
);
|
||||
let denial = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW).1;
|
||||
assert_eq!(denial.code, EXIT_VERSION);
|
||||
assert_eq!(denial.reason, "unsupported_envelope_version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_authorization_id_shape_is_refused() {
|
||||
let path = write_envelope(
|
||||
"env-uuid",
|
||||
&envelope_json(TEST_TARGET, "req-1", 1, 1_700_000_600),
|
||||
);
|
||||
let denial = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW).1;
|
||||
assert_eq!(denial.code, EXIT_BAD_TOKEN);
|
||||
assert_eq!(denial.reason, "authorization_id_not_uuid_v4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_target_must_match_host_identity() {
|
||||
std::env::set_var("REDFLAG_AGENT_ID", TEST_TARGET);
|
||||
let path = write_envelope(
|
||||
"env-target",
|
||||
&envelope_json("some-other-body", GOOD_AUTHORIZATION_ID, 1, 1_700_000_600),
|
||||
);
|
||||
let denial = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW).1;
|
||||
assert_eq!(denial.code, EXIT_AGENT_MISMATCH);
|
||||
assert_eq!(denial.reason, "envelope_target_mismatch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_lifetime_ceiling_is_enforced() {
|
||||
std::env::set_var("REDFLAG_AGENT_ID", TEST_TARGET);
|
||||
let over = 1_700_000_001 + mutation_protocol::MAX_AUTHORIZATION_LIFETIME_SECS + 1;
|
||||
let path = write_envelope(
|
||||
"env-lifetime",
|
||||
&envelope_json(TEST_TARGET, GOOD_AUTHORIZATION_ID, 1, over),
|
||||
);
|
||||
let denial = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW).1;
|
||||
assert_eq!(denial.code, EXIT_TIME_WINDOW);
|
||||
assert_eq!(denial.reason, "authorization_lifetime_exceeded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_timestamp_arithmetic_fails_closed() {
|
||||
std::env::set_var("REDFLAG_AGENT_ID", TEST_TARGET);
|
||||
let body = envelope_json(TEST_TARGET, GOOD_AUTHORIZATION_ID, 1, i64::MAX)
|
||||
.replace("\"not_before\":1700000001", &format!("\"not_before\":{}", i64::MIN));
|
||||
let path = write_envelope("env-time-overflow", &body);
|
||||
let denial = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW).1;
|
||||
assert_eq!(denial.code, EXIT_TIME_WINDOW);
|
||||
assert_eq!(denial.reason, "authorization_time_window_invalid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_backend_payload_must_be_a_json_object() {
|
||||
let manifest = MutationManifest {
|
||||
protocol_version: 1,
|
||||
operation_id: "op".into(),
|
||||
target_id: TEST_TARGET.into(),
|
||||
backend: "pacman".into(),
|
||||
operation: "install".into(),
|
||||
resolved_actions: vec![mutation_protocol::ResolvedAction {
|
||||
kind: "package".into(),
|
||||
identity: "acl@2.3.2-1".into(),
|
||||
payload: "[\"core\"]".into(),
|
||||
}],
|
||||
evidence: vec![],
|
||||
};
|
||||
let denial = validate_backend_payload_shape(&manifest).unwrap_err();
|
||||
assert_eq!(denial.code, EXIT_BAD_TOKEN);
|
||||
assert_eq!(denial.reason, "backend_payload_not_object");
|
||||
}
|
||||
|
||||
// The whole point of cut 2: a well-formed envelope walks the real pipeline
|
||||
// as far as the trusted keyring, and nothing it does burns a replay slot.
|
||||
#[test]
|
||||
fn envelope_reaches_the_keyring_and_never_records_replay() {
|
||||
std::env::set_var("REDFLAG_AGENT_ID", TEST_TARGET);
|
||||
let dir = trust_fixture("env-keyring");
|
||||
let state = dir.join("consumed-tokens");
|
||||
std::env::set_var("REDFLAG_HELPER_KEYRING", dir.join("trusted-keys"));
|
||||
std::env::set_var("REDFLAG_HELPER_STATE", &state);
|
||||
|
||||
let path = write_envelope(
|
||||
"env-pipeline",
|
||||
&envelope_json(TEST_TARGET, GOOD_AUTHORIZATION_ID, 1, 1_700_000_600),
|
||||
);
|
||||
let denial = run_envelope(path.to_str().unwrap(), ENVELOPE_NOW).1;
|
||||
|
||||
// Unprivileged tests cannot own a root keyring, so the keyring step is
|
||||
// where a valid envelope stops — which is the ordering being asserted.
|
||||
assert!(
|
||||
denial.code == EXIT_TRUST_PATH || denial.code == EXIT_KEY_NOT_FOUND,
|
||||
"expected to reach the keyring, got {} ({})",
|
||||
denial.code,
|
||||
denial.reason
|
||||
);
|
||||
assert!(!state.exists(), "envelope path wrote a replay record");
|
||||
|
||||
std::env::remove_var("REDFLAG_HELPER_KEYRING");
|
||||
std::env::remove_var("REDFLAG_HELPER_STATE");
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// verify-envelope — dormant mutation-envelope verification (ARCH-002 cut 2)
|
||||
//
|
||||
// Proves the replacement authority object can enter this helper, be understood
|
||||
// independently, and be refused safely. No backend is executable through it, so
|
||||
// every invocation ends in a typed refusal and leaves a receipt.
|
||||
//
|
||||
// Deliberately absent: the replay ledger. Execute mode records a token only
|
||||
// after a plan is built, so an operation that cannot run never burns its slot.
|
||||
// Nothing here can run; nothing here records. Replay stays unproven until the
|
||||
// first backend migrates.
|
||||
//
|
||||
// Also deliberately absent: artifact custody. When a backend carries a local
|
||||
// cached artifact as executor input, the helper must rehash it before use.
|
||||
// Remote repository execution keeps the limits RAF already states.
|
||||
// ============================================================================
|
||||
|
||||
fn read_envelope_from_file(path: &str) -> Result<MutationEnvelope, Denial> {
|
||||
let raw = fs::read_to_string(path).map_err(|e| {
|
||||
Denial::new(EXIT_BAD_TOKEN, "envelope_file_read_failed", format!("{}: {}", path, e))
|
||||
})?;
|
||||
serde_json::from_str(&raw)
|
||||
.map_err(|e| Denial::new(EXIT_BAD_TOKEN, "envelope_parse_failed", e.to_string()))
|
||||
}
|
||||
|
||||
// Enough shape to fail closed without inventing backend semantics: the common
|
||||
// layer never reads inside a payload, but it refuses one that could not be a
|
||||
// backend action in the first place.
|
||||
fn validate_backend_payload_shape(manifest: &MutationManifest) -> Result<(), Denial> {
|
||||
for (index, action) in manifest.resolved_actions.iter().enumerate() {
|
||||
match serde_json::from_str::<serde_json::Value>(&action.payload) {
|
||||
Ok(serde_json::Value::Object(_)) => {}
|
||||
Ok(_) => {
|
||||
return Err(Denial::new(
|
||||
EXIT_BAD_TOKEN,
|
||||
"backend_payload_not_object",
|
||||
format!("resolved action {} identity={}", index, action.identity),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Denial::new(
|
||||
EXIT_BAD_TOKEN,
|
||||
"backend_payload_not_json",
|
||||
format!("resolved action {}: {}", index, e),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Always denies. Ordered so each refusal gets its own exit code instead of one
|
||||
// opaque verification failure.
|
||||
fn run_envelope(envelope_file: &str, now: i64) -> (Option<MutationEnvelope>, Denial) {
|
||||
let envelope = match read_envelope_from_file(envelope_file) {
|
||||
Ok(e) => e,
|
||||
Err(d) => return (None, d),
|
||||
};
|
||||
|
||||
// Evaluate the denial before moving the envelope into the return — every
|
||||
// detail string reads from it.
|
||||
macro_rules! deny {
|
||||
($d:expr) => {{
|
||||
let denial = $d;
|
||||
return (Some(envelope), denial);
|
||||
}};
|
||||
}
|
||||
|
||||
if envelope.manifest.protocol_version != mutation_protocol::MUTATION_PROTOCOL_VERSION
|
||||
|| envelope.authorization.protocol_version != mutation_protocol::MUTATION_PROTOCOL_VERSION
|
||||
{
|
||||
deny!(Denial::new(
|
||||
EXIT_VERSION,
|
||||
"unsupported_envelope_version",
|
||||
format!(
|
||||
"manifest={} authorization={} supported={}",
|
||||
envelope.manifest.protocol_version,
|
||||
envelope.authorization.protocol_version,
|
||||
mutation_protocol::MUTATION_PROTOCOL_VERSION
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(e) = envelope.manifest.validate() {
|
||||
deny!(Denial::new(EXIT_BAD_TOKEN, "manifest_invalid", e));
|
||||
}
|
||||
|
||||
if !mutation_protocol::is_canonical_uuid_v4(&envelope.authorization.authorization_id) {
|
||||
deny!(Denial::new(
|
||||
EXIT_BAD_TOKEN,
|
||||
"authorization_id_not_uuid_v4",
|
||||
"authorization_id must be a canonical UUID v4 before it can key a replay ledger",
|
||||
));
|
||||
}
|
||||
|
||||
// target_id is the RedFlag agent identity. Both copies are signed and the
|
||||
// verifier requires them equal; both are compared anyway, because the cost
|
||||
// is a string compare and the failure would be silent.
|
||||
let local = match local_agent_id() {
|
||||
Ok(v) => v,
|
||||
Err(d) => deny!(d),
|
||||
};
|
||||
let targets = [
|
||||
("manifest", envelope.manifest.target_id.clone()),
|
||||
("authorization", envelope.authorization.target_id.clone()),
|
||||
];
|
||||
for (field, value) in targets {
|
||||
if value != local {
|
||||
deny!(Denial::new(
|
||||
EXIT_AGENT_MISMATCH,
|
||||
"envelope_target_mismatch",
|
||||
format!("{}_target_id={} host_agent_id={}", field, value, local),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if now < envelope.authorization.not_before {
|
||||
deny!(Denial::new(
|
||||
EXIT_TIME_WINDOW,
|
||||
"envelope_not_yet_valid",
|
||||
format!("now={} not_before={}", now, envelope.authorization.not_before),
|
||||
));
|
||||
}
|
||||
if now > envelope.authorization.expires_at {
|
||||
deny!(Denial::new(
|
||||
EXIT_TIME_WINDOW,
|
||||
"envelope_expired",
|
||||
format!("now={} expires_at={}", now, envelope.authorization.expires_at),
|
||||
));
|
||||
}
|
||||
let lifetime = match envelope
|
||||
.authorization
|
||||
.expires_at
|
||||
.checked_sub(envelope.authorization.not_before)
|
||||
{
|
||||
Some(value) if value > 0 => value,
|
||||
_ => deny!(Denial::new(
|
||||
EXIT_TIME_WINDOW,
|
||||
"authorization_time_window_invalid",
|
||||
format!(
|
||||
"not_before={} expires_at={}",
|
||||
envelope.authorization.not_before, envelope.authorization.expires_at
|
||||
),
|
||||
)),
|
||||
};
|
||||
if lifetime > mutation_protocol::MAX_AUTHORIZATION_LIFETIME_SECS {
|
||||
deny!(Denial::new(
|
||||
EXIT_TIME_WINDOW,
|
||||
"authorization_lifetime_exceeded",
|
||||
format!(
|
||||
"lifetime={}s ceiling={}s",
|
||||
lifetime,
|
||||
mutation_protocol::MAX_AUTHORIZATION_LIFETIME_SECS
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if envelope.authorization.decision != "allow" {
|
||||
deny!(Denial::new(
|
||||
EXIT_AUTHORIZATION_DENIED,
|
||||
"authorization_decision_not_allow",
|
||||
format!("decision={}", envelope.authorization.decision),
|
||||
));
|
||||
}
|
||||
|
||||
let keyring_dir = PathBuf::from(env_or("REDFLAG_HELPER_KEYRING", DEFAULT_KEYRING_DIR));
|
||||
let keyring = match load_keyring(&keyring_dir) {
|
||||
Ok(k) => k,
|
||||
Err(d) => deny!(d),
|
||||
};
|
||||
let verifying_key = match keyring
|
||||
.iter()
|
||||
.find(|(id, _)| id == &envelope.authorization.key_id)
|
||||
.map(|(_, vk)| *vk)
|
||||
{
|
||||
Some(vk) => vk,
|
||||
None => deny!(Denial::new(
|
||||
EXIT_KEY_NOT_FOUND,
|
||||
"key_id_not_trusted",
|
||||
format!("key_id={}", envelope.authorization.key_id),
|
||||
)),
|
||||
};
|
||||
|
||||
if let Err(e) = envelope
|
||||
.authorization
|
||||
.verify_for_execution_at(&verifying_key, &envelope.manifest, now)
|
||||
{
|
||||
deny!(Denial::new(EXIT_SIGNATURE, "envelope_verification_failed", e));
|
||||
}
|
||||
|
||||
if let Err(d) = validate_backend_payload_shape(&envelope.manifest) {
|
||||
deny!(d);
|
||||
}
|
||||
|
||||
let backend = envelope.manifest.backend.clone();
|
||||
log_security(&format!(
|
||||
"envelope_verified operation_id={} manifest_hash={} authorization_id={} backend={} target_id={}",
|
||||
envelope.manifest.operation_id,
|
||||
envelope.manifest.hash(),
|
||||
envelope.authorization.authorization_id,
|
||||
backend,
|
||||
envelope.manifest.target_id
|
||||
));
|
||||
deny!(Denial::new(
|
||||
EXIT_UNSUPPORTED_OP,
|
||||
"backend_not_migrated",
|
||||
format!("backend={} has no executable path through the envelope yet", backend),
|
||||
))
|
||||
}
|
||||
|
||||
fn run_verify_envelope_cli(args: &[String]) -> i32 {
|
||||
let mut envelope_file: Option<String> = None;
|
||||
let mut receipt_file: Option<String> = None;
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--envelope-file" => {
|
||||
envelope_file = args.get(i + 1).cloned();
|
||||
i += 2;
|
||||
}
|
||||
"--receipt-file" => {
|
||||
receipt_file = args.get(i + 1).cloned();
|
||||
i += 2;
|
||||
}
|
||||
other => {
|
||||
log_error(&format!("verify-envelope unknown argument {}", other));
|
||||
return EXIT_BAD_TOKEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
let envelope_file = match envelope_file {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
log_error("verify-envelope usage: redflag-helper verify-envelope --envelope-file <path> [--receipt-file <path>]");
|
||||
return EXIT_BAD_TOKEN;
|
||||
}
|
||||
};
|
||||
|
||||
let now = now_unix();
|
||||
let (envelope, denial) = run_envelope(&envelope_file, now);
|
||||
let decision = if denial.code == EXIT_EXEC_FAILED { "failed" } else { "denied" };
|
||||
log_security(&format!(
|
||||
"{} reason={} detail={} exit={}",
|
||||
decision, denial.reason, denial.detail, denial.code
|
||||
));
|
||||
|
||||
let receipt = match envelope {
|
||||
Some(e) => e.receipt_for(
|
||||
MutationOutcome {
|
||||
decision: decision.to_string(),
|
||||
reason: denial.reason.to_string(),
|
||||
executed: false,
|
||||
verified_actions: 0,
|
||||
exit_code: denial.code,
|
||||
detail: denial.detail.clone(),
|
||||
},
|
||||
now,
|
||||
),
|
||||
None => MutationReceipt {
|
||||
protocol_version: mutation_protocol::MUTATION_PROTOCOL_VERSION,
|
||||
operation_id: String::new(),
|
||||
manifest_hash: String::new(),
|
||||
authorization_id: String::new(),
|
||||
target_id: String::new(),
|
||||
backend: String::new(),
|
||||
operation: String::new(),
|
||||
decision: decision.to_string(),
|
||||
reason: denial.reason.to_string(),
|
||||
executed: false,
|
||||
verified_actions: 0,
|
||||
exit_code: denial.code,
|
||||
error: denial.detail.clone(),
|
||||
timestamp: now,
|
||||
},
|
||||
};
|
||||
if let Some(rp) = receipt_file {
|
||||
emit_result_to_file(&receipt, &rp);
|
||||
}
|
||||
emit_result(&receipt);
|
||||
denial.code
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -2190,6 +2586,12 @@ fn main() {
|
|||
std::process::exit(run_mint_cli(&args[2..]));
|
||||
}
|
||||
|
||||
// "verify-envelope" is the dormant mutation-envelope path. It verifies and
|
||||
// refuses; no backend executes through it yet.
|
||||
if args.get(1).map(|s| s.as_str()) == Some("verify-envelope") {
|
||||
std::process::exit(run_verify_envelope_cli(&args[2..]));
|
||||
}
|
||||
|
||||
// --token-file <path> reads the capability token from a file instead of stdin.
|
||||
// This avoids SCM_RIGHTS fd-passing via systemd-run --pipe, which dbus-broker 37
|
||||
// on Fedora 43 drops (MSG_CTRUNC) for new-connection handshake messages.
|
||||
|
|
|
|||
Loading…
Reference in a new issue