Skip to content
1 change: 1 addition & 0 deletions crates/auths-cli/src/commands/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ fn handle_explain(cmd: ExplainCommand) -> Result<()> {
Outcome::Deny => out.error("DENY"),
Outcome::Indeterminate => out.warn("INDETERMINATE"),
Outcome::RequiresApproval => out.warn("REQUIRES_APPROVAL"),
_ => out.warn(&format!("{:?}", decision.outcome)),
};
out.println(&format!("Decision: {}", decision_str));
out.println(&format!(" Reason: {:?}", decision.reason));
Expand Down
1 change: 1 addition & 0 deletions crates/auths-cli/src/commands/verify_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
}],
bundle_timestamp: Utc::now(),
max_valid_for_secs: 86400,
Expand Down
1 change: 1 addition & 0 deletions crates/auths-cli/tests/cases/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn create_signed_attestation(
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};

// Create canonical data for signing (includes org fields)
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-core/src/policy/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl Action {
/// capabilities: vec![Capability::sign_commit()],
/// delegated_by: None,
/// signer_type: None,
/// environment_claim: None,
/// };
///
/// let decision = authorize_device(
Expand Down Expand Up @@ -197,6 +198,7 @@ mod tests {
capabilities,
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-core/src/policy/org.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ use super::device::Action;
/// capabilities: vec![Capability::manage_members()],
/// delegated_by: None,
/// signer_type: None,
/// environment_claim: None,
/// };
///
/// let decision = authorize_org_action(
Expand Down Expand Up @@ -199,6 +200,7 @@ mod tests {
capabilities,
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/auths-id/src/attestation/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ pub fn create_signed_attestation(
capabilities,
delegated_by,
signer_type: None,
environment_claim: None,
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/auths-id/src/attestation/revoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,6 @@ pub fn create_signed_revocation(
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
})
}
1 change: 1 addition & 0 deletions crates/auths-id/src/attestation/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/auths-id/src/domain/attestation_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-id/src/policy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub fn context_from_attestation(
auths_verifier::core::SignerType::Human => auths_policy::SignerType::Human,
auths_verifier::core::SignerType::Agent => auths_policy::SignerType::Agent,
auths_verifier::core::SignerType::Workload => auths_policy::SignerType::Workload,
_ => auths_policy::SignerType::Workload,
};
ctx = ctx.signer_type(policy_st);
}
Expand Down Expand Up @@ -454,6 +455,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/auths-id/src/storage/registry/org_member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};
let now = Utc::now();
assert_eq!(compute_status(&att, now), MemberStatus::Active);
Expand All @@ -313,6 +314,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};
let now = Utc::now();
assert_eq!(compute_status(&att, now), MemberStatus::Revoked);
Expand All @@ -338,6 +340,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};
let now = Utc::now();
assert!(matches!(
Expand Down Expand Up @@ -366,6 +369,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};
let now = Utc::now();
assert_eq!(compute_status(&att, now), MemberStatus::Active);
Expand All @@ -391,6 +395,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};
// Uses <= for expiry, so exactly at boundary = expired
assert!(matches!(
Expand Down Expand Up @@ -446,6 +451,7 @@ mod tests {
capabilities: vec![Capability::sign_commit(), Capability::sign_release()],
delegated_by: None,
signer_type: None,
environment_claim: None,
};

let vec = attestation_capability_vec(&att);
Expand Down
1 change: 1 addition & 0 deletions crates/auths-id/src/testing/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,6 @@ pub fn test_attestation(device_did: &DeviceDID, issuer: &str) -> Attestation {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}
1 change: 1 addition & 0 deletions crates/auths-policy/src/compiled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob};
/// Constructed only via [`compile`](crate::compile::compile). Cannot be built directly.
/// All string fields have been parsed into canonical typed forms.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum CompiledExpr {
/// Always allow.
True,
Expand Down
13 changes: 13 additions & 0 deletions crates/auths-policy/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use std::collections::HashMap;

use chrono::{DateTime, Utc};
use serde_json::Value;

use crate::approval::ApprovalAttestation;
use crate::types::{CanonicalCapability, CanonicalDid, SignerType};
Expand Down Expand Up @@ -77,6 +78,11 @@ pub struct EvalContext {
// ── Approval Attestations ────────────────────────────────────────
/// Submitted approval attestations for ApprovalGate checking.
pub approvals: Vec<ApprovalAttestation>,

// ── Audit Metadata ────────────────────────────────────────────────
/// Opaque gateway metadata (source IP, request ID) for audit logging.
/// Not used in policy evaluation — carried through for audit trail.
pub audit_metadata: HashMap<String, Value>,
}

impl EvalContext {
Expand Down Expand Up @@ -104,6 +110,7 @@ impl EvalContext {
workload_claims: HashMap::new(),
attrs: HashMap::new(),
approvals: Vec::new(),
audit_metadata: HashMap::new(),
}
}

Expand Down Expand Up @@ -225,6 +232,12 @@ impl EvalContext {
self.approvals.push(attestation);
self
}

/// Add an audit metadata entry.
pub fn audit_meta(mut self, key: impl Into<String>, value: Value) -> Self {
self.audit_metadata.insert(key.into(), value);
self
}
}

#[cfg(test)]
Expand Down
5 changes: 5 additions & 0 deletions crates/auths-policy/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub struct Decision {

/// The outcome of a policy evaluation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Outcome {
/// The action is allowed.
Allow,
Expand All @@ -35,13 +36,16 @@ pub enum Outcome {
/// The action requires human approval before proceeding.
/// Propagated through `evaluate_strict` (NOT collapsed to Deny).
RequiresApproval,
/// No attestation was provided. Distinct from Deny (attestation was invalid).
MissingCredential,
}

/// Machine-readable reason code for stable logging and alerting.
///
/// These codes are designed to be stable across versions for use in
/// monitoring dashboards, alerting rules, and audit queries.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ReasonCode {
/// Unconditional allow/deny (True/False expressions).
Unconditional,
Expand Down Expand Up @@ -176,6 +180,7 @@ impl std::fmt::Display for Outcome {
Outcome::Deny => write!(f, "DENY"),
Outcome::Indeterminate => write!(f, "INDETERMINATE"),
Outcome::RequiresApproval => write!(f, "REQUIRES_APPROVAL"),
Outcome::MissingCredential => write!(f, "MISSING_CREDENTIAL"),
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions crates/auths-policy/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ pub fn evaluate_strict(policy: &CompiledPolicy, ctx: &EvalContext) -> Decision {
),
)
.with_policy_hash(*policy.source_hash()),
Outcome::Allow | Outcome::Deny | Outcome::RequiresApproval => decision,
Outcome::Allow | Outcome::Deny | Outcome::RequiresApproval | Outcome::MissingCredential => {
decision
}
}
}

Expand Down Expand Up @@ -111,7 +113,9 @@ fn eval_expr(expr: &CompiledExpr, ctx: &EvalContext) -> Decision {
ReasonCode::CombinatorResult,
format!("NOT({})", result.message),
),
Outcome::Indeterminate | Outcome::RequiresApproval => result,
Outcome::Indeterminate | Outcome::RequiresApproval | Outcome::MissingCredential => {
result
}
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/auths-policy/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize};
/// Compilation validates and canonicalizes all string fields.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", content = "args")]
#[non_exhaustive]
pub enum Expr {
// ── Combinators ──────────────────────────────────────────────────
/// Always allow.
Expand Down
1 change: 1 addition & 0 deletions crates/auths-policy/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ impl fmt::Display for CanonicalCapability {
/// Used to distinguish human, AI agent, and workload (CI/CD) signers
/// in policy evaluation.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SignerType {
/// A human user.
Human,
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-radicle/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ impl TryFrom<RadAttestation> for Attestation {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
})
}
}
Expand Down Expand Up @@ -507,6 +508,7 @@ mod tests {
capabilities: vec![],
delegated_by: None,
signer_type: None,
environment_claim: None,
};

let rad: RadAttestation = (&core).try_into().unwrap();
Expand Down
6 changes: 6 additions & 0 deletions crates/auths-radicle/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ pub fn decision_to_verify_result(decision: Decision) -> VerifyResult {
message: format!("approval required: {}", decision.message),
},
},
_ => VerifyResult::Rejected {
reason: RejectReason::PolicyDenied {
message: decision.message,
},
},
}
}

Expand Down Expand Up @@ -602,6 +607,7 @@ mod tests {
.collect(),
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/auths-radicle/tests/cases/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub fn make_test_attestation(
capabilities,
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/auths-sdk/src/workflows/org.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ pub fn add_organization_member(
capabilities: parsed_caps,
delegated_by: Some(IdentityDID::new(admin_att.subject.to_string())),
signer_type: None,
environment_claim: None,
};

backend
Expand Down
3 changes: 3 additions & 0 deletions crates/auths-sdk/src/workflows/policy_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ fn collect_predicates_rec(expr: &Expr, predicates: &mut HashSet<String>) {
predicates.insert(format!("ApprovalGate(approvers={:?})", approvers));
collect_predicates_rec(inner, predicates);
}
_ => {
predicates.insert(format!("{:?}", expr));
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-sdk/tests/cases/org.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ fn base_admin_attestation() -> Attestation {
capabilities: vec![Capability::sign_commit(), Capability::manage_members()],
delegated_by: None,
signer_type: None,
environment_claim: None,
}
}

Expand All @@ -67,6 +68,7 @@ fn base_member_attestation() -> Attestation {
capabilities: vec![Capability::sign_commit()],
delegated_by: Some(IdentityDID::new(ADMIN_DID)),
signer_type: None,
environment_claim: None,
}
}

Expand Down
Loading
Loading