# === Policy Engine (src_policy.rs) v1 ===
#  Dependencies: regex, serde, serde_yaml, serde_json, std::path, std::time, crate::vault
# Deterministic policy engine for AI agent tool call evaluation. Loads YAML policy rules, compiles regex patterns, evaluates tool calls against conditions (regex, parameter checks, vault queries), and returns Allow/Deny/Ask/Gate/Ensure decisions. First-match-wins evaluation. No NLP, no network. Includes self-protection rules that prevent AI from tampering with policy infrastructure.

# Module invariants:
#   - KNOWN_CONDITION_FNS: List of valid condition function names for validation
#   - First-match-wins rule evaluation order is always preserved
#   - Self-protection rules are always first in default_policy() evaluation order
#   - protect_checks_dir is always the first rule in self_protection_rules()
#   - All self-protection rules have locked=true
#   - Gate actions without vault fail closed (deny)
#   - Condition evaluation errors cause rule to not match (fail-safe)
#   - Invalid regexes in PolicyConfig are silently filtered during compilation
#   - default_gate_within() = 50
#   - default_ensure_timeout() = 5
#   - default_version() = 1
#   - default_allow() = Decision::Allow

class Decision(Enum):
    """Policy evaluation decision enum with lowercase conversion method"""
    Allow = "Allow"
    Deny = "Deny"
    Ask = "Ask"
    Gate = "Gate"
    Ensure = "Ensure"

class GateConfig:
    """Configuration for Gate action: requires a prior allowed action within N recent actions"""
    requires_prior: String                   # required, Tool name or detail pattern to search for in action log. Pipe-delimited OR supported (e.g., 'EnterPlanMode|TaskCreate')
    within: u32                              # required, Look back this many recent allowed actions

class EnsureConfig:
    """Configuration for Ensure action: run a check script before allowing"""
    check: String                            # required, Script name in .signet/checks/ to execute
    timeout: u32                             # required, Execution timeout in seconds
    message: String                          # required, User-facing message if check fails

class PolicyRule:
    """Single policy rule: regex pattern + conditions + action + metadata"""
    name: String                             # required, Rule identifier
    tool_pattern: String                     # required, Regex pattern matching tool names
    conditions: Vec<String>                  # required, List of condition expressions (all must pass)
    action: Decision                         # required, Action to take if rule matches
    reason: Option<String>                   # required, Human-readable reason for the action
    alternative: Option<String>              # required, Suggested alternative if denied
    locked: bool                             # required, If true, rule protects policy infrastructure and cannot be bypassed
    gate: Option<GateConfig>                 # required, Gate configuration (required if action=Gate)
    ensure: Option<EnsureConfig>             # required, Ensure configuration (required if action=Ensure)

class PolicyConfig:
    """Top-level policy configuration loaded from YAML"""
    version: u32                             # required, Policy schema version
    rules: Vec<PolicyRule>                   # required, Ordered list of policy rules
    default_action: Decision                 # required, Action when no rules match

class CompiledRule:
    """Policy rule with compiled regex for runtime evaluation"""
    name: String                             # required
    tool_regex: Regex                        # required, Compiled regex from tool_pattern
    conditions: Vec<String>                  # required
    action: Decision                         # required
    reason: Option<String>                   # required
    alternative: Option<String>              # required
    locked: bool                             # required
    gate: Option<GateConfig>                 # required
    ensure: Option<EnsureConfig>             # required

class CompiledPolicy:
    """Runtime policy with all rules compiled"""
    rules: Vec<CompiledRule>                 # required, Compiled rules in evaluation order
    default_action: Decision                 # required

class EvaluationResult:
    """Result of evaluating a tool call against policy"""
    decision: Decision                       # required, Final decision (Allow/Deny/Ask/Gate/Ensure)
    matched_rule: Option<String>             # required, Name of the rule that matched (None if default action used)
    matched_locked: bool                     # required, True if the matched rule is a locked self-protection rule
    reason: Option<String>                   # required, Human-readable reason for the decision
    evaluation_time_us: u64                  # required, Evaluation duration in microseconds
    ensure_config: Option<EnsureConfig>      # required, Ensure configuration if decision=Ensure

class ToolCall:
    """Tool call being evaluated: name + JSON parameters"""
    tool_name: String                        # required, Name of the tool being invoked
    parameters: serde_json::Value            # required, JSON object of tool parameters

def as_lowercase(
    self: &Decision,
) -> &'static str:
    """
    Convert Decision enum variant to lowercase string representation

    Postconditions:
      - Returns lowercase string matching enum variant name

    Side effects: none
    Idempotent: no
    """
    ...

def default_gate_within() -> u32:
    """
    Default value for GateConfig.within field (serde default)

    Postconditions:
      - Returns 50

    Side effects: none
    Idempotent: no
    """
    ...

def default_ensure_timeout() -> u32:
    """
    Default value for EnsureConfig.timeout field (serde default)

    Postconditions:
      - Returns 5

    Side effects: none
    Idempotent: no
    """
    ...

def default_version() -> u32:
    """
    Default value for PolicyConfig.version field (serde default)

    Postconditions:
      - Returns 1

    Side effects: none
    Idempotent: no
    """
    ...

def default_allow() -> Decision:
    """
    Default value for PolicyConfig.default_action field (serde default)

    Postconditions:
      - Returns Decision::Allow

    Side effects: none
    Idempotent: no
    """
    ...

def from_config(
    config: &PolicyConfig,
) -> CompiledPolicy:
    """
    Compile PolicyConfig into CompiledPolicy by compiling all rule regexes. Rules with invalid regexes are silently skipped.

    Postconditions:
      - Returns CompiledPolicy with successfully compiled rules only
      - Rules with invalid regex patterns are filtered out
      - Rule order from config is preserved

    Side effects: none
    Idempotent: no
    """
    ...

def evaluate_condition(
    condition: &str,
    call: &ToolCall,
    vault: Option<&Vault>,
) -> Result<bool, String>:
    """
    Evaluate a single condition expression against a tool call. Supports: contains(), param_eq(), param_gt(), param_lt(), param_ne(), param_contains(), matches(), spend_gt(), spend_plus_amount_gt(), has_credential(), has_recent_action(), not(), or(), true, false, any_of(), contains_word(). Returns Ok(true) if condition passes, Ok(false) if fails, Err if invalid syntax or regex error.

    Postconditions:
      - Returns Ok(true) if all checks in condition pass
      - Returns Ok(false) if any check fails
      - Returns Err for unknown function names, invalid regex in matches(), parse errors

    Errors:
      - unknown_condition (String): Condition function name not in KNOWN_CONDITION_FNS
      - regex_compile_error (String): Invalid regex pattern in matches() condition
      - parse_error (String): Cannot parse numeric threshold in param_gt/param_lt/spend_gt

    Side effects: Queries vault if condition uses spend_gt, spend_plus_amount_gt, has_credential, or has_recent_action
    Idempotent: no
    """
    ...

def evaluate(
    call: &ToolCall,
    policy: &CompiledPolicy,
    vault: Option<&Vault>,
) -> EvaluationResult:
    """
    Evaluate a tool call against compiled policy. First-match-wins: iterates rules in order, returns on first match. Gate actions are resolved inline by querying vault. Ensure actions return decision=Ensure with config for caller to execute. Returns EvaluationResult with decision, matched rule, timing.

    Postconditions:
      - Returns first matched rule's action, or default_action if no match
      - Gate actions are resolved to Allow or Deny based on vault query
      - Ensure actions return decision=Ensure with ensure_config populated
      - evaluation_time_us is always populated with microsecond-precision timing
      - Condition evaluation errors cause the rule to not match (fall through to next rule)

    Side effects: Queries vault for Gate resolution and condition evaluation (read-only)
    Idempotent: no
    """
    ...

def resolve_gate(
    config: &GateConfig,
    vault: Option<&Vault>,
) -> bool:
    """
    Resolve a Gate action by checking vault's action log for the required prior action. Fails closed: returns false if vault is None.

    Postconditions:
      - Returns true if vault.has_recent_allowed_action() finds the requires_prior pattern within the specified window
      - Returns false if vault is None (fail-closed)
      - Returns false if no matching action found in vault

    Side effects: Queries vault action log (read-only)
    Idempotent: no
    """
    ...

def load_policy(
    path: &Path,
) -> CompiledPolicy:
    """
    Load policy from YAML file. Falls back to default_policy() if file read or parse fails. Never panics.

    Postconditions:
      - Returns compiled policy from file if read and parse succeed
      - Returns default_policy() if file cannot be read or YAML parse fails
      - Never returns an error or panics

    Side effects: Reads YAML file from filesystem
    Idempotent: no
    """
    ...

def load_policy_config(
    path: &Path,
) -> Result<PolicyConfig, String>:
    """
    Load raw PolicyConfig from YAML file. Returns error if file cannot be read or YAML is invalid.

    Postconditions:
      - Returns parsed PolicyConfig on success

    Errors:
      - file_read_error (String): Cannot read file at path
      - yaml_parse_error (String): YAML deserialization fails

    Side effects: Reads YAML file from filesystem
    Idempotent: no
    """
    ...

def validate_policy(
    config: &PolicyConfig,
) -> Vec<String>:
    """
    Validate a PolicyConfig: check regex patterns compile, condition functions are known, Gate/Ensure configs are present and valid. Returns list of error strings (empty = valid).

    Postconditions:
      - Returns empty Vec if policy is valid
      - Returns Vec of error messages for: invalid regex, unknown condition functions, missing/invalid Gate config, missing/invalid Ensure config, out-of-range gate.within or ensure.timeout

    Side effects: none
    Idempotent: no
    """
    ...

def validate_ensure_check_name(
    name: &str,
) -> Result<(), String>:
    """
    Validate that a check script name is safe: no slashes, no .., no null bytes, no control chars, not empty.

    Postconditions:
      - Returns Ok(()) if name is safe

    Errors:
      - empty_name (String): name.is_empty()
      - path_separator (String): name contains '/' or '\\'
      - path_traversal (String): name contains '..'
      - null_byte (String): name contains null byte
      - control_char (String): name contains control characters (< 0x20, except tab)

    Side effects: none
    Idempotent: no
    """
    ...

def resolve_ensure_script_path(
    check_name: &str,
) -> Result<std::path::PathBuf, String>:
    """
    Resolve and validate the full path for an ensure check script in .signet/checks/. Returns canonicalized path if valid, error if path escapes checks directory or cannot be resolved.

    Preconditions:
      - check_name passes validate_ensure_check_name()

    Postconditions:
      - Returns canonicalized path if script exists and is within checks directory
      - Path is guaranteed to be within .signet/checks/

    Errors:
      - invalid_name (String): check_name fails validate_ensure_check_name()
      - cannot_resolve (String): canonicalize() fails (file not found or permission error)
      - path_escape (String): Canonical path is not within checks directory

    Side effects: Calls canonicalize() which accesses filesystem metadata
    Idempotent: no
    """
    ...

def self_protection_rules() -> Vec<PolicyRule>:
    """
    Generate locked self-protection rules that prevent AI from tampering with policy infrastructure. Returns 8 locked rules: protect_checks_dir (highest priority), protect_signet_dir, protect_signet_binary, protect_hook_config, protect_signet_symlink, protect_signet_process, protect_preflight_storage, github_identity_guard.

    Postconditions:
      - Returns exactly 8 PolicyRule instances
      - All rules have locked=true
      - First rule is protect_checks_dir (must be evaluated before all others)
      - Rules protect: .signet/checks/, .signet/, signet-eval binary, settings.json, symlinks to signet, kill/pkill/killall signet, preflight DB, git remote operations

    Side effects: none
    Idempotent: no
    """
    ...

def default_policy() -> CompiledPolicy:
    """
    Generate the default policy: self_protection_rules() + standard safety rules (plan-before-code, protect core files, block rm/force-push/piped-exec/credential-writes/chmod-777, etc). Default action is Allow.

    Postconditions:
      - Returns compiled policy with 8 locked rules + ~8 standard rules
      - Self-protection rules are first in evaluation order
      - Default action is Decision::Allow

    Side effects: none
    Idempotent: no
    """
    ...

def split_at_top_level(
    s: &str,
    separator: &str,
) -> Option<(&str, &str)>:
    """
    Split string at first occurrence of separator where parenthesis depth is 0. Returns Some((left, right)) if found, None otherwise. Used for parsing or() conditions.

    Postconditions:
      - Returns Some((before_sep, after_sep)) if separator found at depth 0
      - Returns None if separator not found at depth 0 or separator is empty
      - Tracks parenthesis depth correctly: '(' increments, ')' decrements (saturating)

    Side effects: none
    Idempotent: no
    """
    ...

def strip_fn(
    s: &str,
    name: &str,
) -> Option<&str>:
    """
    Extract arguments from a function call string. If s matches 'name(args)', returns Some(args). Returns None otherwise.

    Postconditions:
      - Returns Some(args) if s is 'name(args)' with matching parentheses
      - Returns None if s does not start with name or lacks matching parens

    Side effects: none
    Idempotent: no
    """
    ...

def extract_quoted(
    s: &str,
) -> Option<String>:
    """
    Extract string content from single or double quoted string. Returns Some(unquoted_string) if s is quoted, None otherwise.

    Postconditions:
      - Returns Some(content) if s is wrapped in matching single or double quotes
      - Returns None if s is not quoted

    Side effects: none
    Idempotent: no
    """
    ...

def param_str(
    params: &serde_json::Value,
    field: &str,
) -> String:
    """
    Extract string parameter value from JSON parameters object. Returns empty string if field not found or not a string.

    Postconditions:
      - Returns field value as String if present and is string type
      - Returns empty string if field not found or not a string

    Side effects: none
    Idempotent: no
    """
    ...

def param_f64(
    params: &serde_json::Value,
    field: &str,
) -> f64:
    """
    Extract numeric parameter value from JSON parameters object. Tries as_f64() first, then parses as string. Returns 0.0 if field not found or not numeric.

    Postconditions:
      - Returns field value as f64 if present and numeric or parseable string
      - Returns 0.0 if field not found or cannot be parsed as number

    Side effects: none
    Idempotent: no
    """
    ...

# ── REQUIRED EXPORTS ──────────────────────────────────
# Your implementation module MUST export ALL of these names
# with EXACTLY these spellings. Tests import them by name.
# __all__ = ['Decision', 'GateConfig', 'EnsureConfig', 'PolicyRule', 'PolicyConfig', 'CompiledRule', 'CompiledPolicy', 'EvaluationResult', 'ToolCall', 'as_lowercase', 'default_gate_within', 'default_ensure_timeout', 'default_version', 'default_allow', 'from_config', 'evaluate_condition', 'String', 'evaluate', 'resolve_gate', 'load_policy', 'load_policy_config', 'validate_policy', 'validate_ensure_check_name', 'resolve_ensure_script_path', 'self_protection_rules', 'default_policy', 'split_at_top_level', 'strip_fn', 'extract_quoted', 'param_str', 'param_f64']
