-
Notifications
You must be signed in to change notification settings - Fork 0
Feature Phase2 Governance Tasks
Rick Hightower edited this page Jan 28, 2026
·
1 revision
Feature ID: phase2-governance Status: COMPLETE (P2.1, P2.2, P2.3, P2.4 all implemented) Total Estimated Days: 5.5-9 days Completion Date: 2026-01-25
- Create
PolicyModeenum inmodels/mod.rs - Values:
Enforce,Warn,Audit - Implement
Defaulttrait (default = Enforce) - Implement
Deserializefor YAML parsing (case-insensitive) - Implement
Serializefor JSON output - Add unit tests for parsing
Code:
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PolicyMode {
#[default]
Enforce,
Warn,
Audit,
}- Create
RuleMetadatastruct inmodels/mod.rs - Fields:
author,created_by,reason,confidence,last_reviewed,ticket,tags - All fields are
Option<T> - Create
Confidenceenum:High,Medium,Low - Implement
DeserializeandSerialize - Add unit tests
Code:
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RuleMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<Confidence>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_reviewed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ticket: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
High,
Medium,
Low,
}- Add
mode: Option<PolicyMode>field toRule - Add
priority: Option<i32>field toRule - Add
metadata: Option<RuleMetadata>field toRule - Use
#[serde(default)]for backward compatibility - Update existing tests to verify backward compatibility
- Add new tests for parsing rules with governance fields
- Create function
sort_rules_by_priority(rules: &mut Vec<Rule>) - Sort by priority descending (higher first)
- Stable sort to preserve file order for same priority
- Default priority = 0 for rules without explicit priority
- Call sorting before rule matching in hook processor
- Add unit tests for sorting behavior
Code:
pub fn sort_rules_by_priority(rules: &mut [Rule]) {
rules.sort_by(|a, b| {
let priority_a = a.priority.unwrap_or(0);
let priority_b = b.priority.unwrap_or(0);
priority_b.cmp(&priority_a) // Descending order
});
}- Update
execute_actionto check rule mode -
Enforce: Current behavior (block/inject/run) -
Warn: Never block, inject warning message instead -
Audit: Skip action, log only - Create warning context injection for warn mode
- Add integration tests for each mode
Mode Execution Logic:
fn execute_action(rule: &Rule, action: &Action, event: &Event) -> ActionResult {
let mode = rule.mode.unwrap_or_default();
match mode {
PolicyMode::Enforce => {
// Normal execution
execute_action_impl(action, event)
}
PolicyMode::Warn => {
// Never block, inject warning instead
if action.is_block() {
ActionResult::Warning(format!("Rule '{}' would block: {}", rule.name, action.reason()))
} else {
execute_action_impl(action, event)
}
}
PolicyMode::Audit => {
// Log only, no execution
ActionResult::Audited
}
}
}- Create
resolve_conflicts(matched_rules: Vec<&Rule>) -> ResolvedOutcome - Enforce mode always wins over warn/audit
- Among same modes, highest priority wins
- For multiple blocks, use highest priority block message
- Log conflict resolution decisions
- Add unit tests for all conflict scenarios
Conflict Resolution Table Tests:
#[test]
fn test_enforce_wins_over_warn() { ... }
#[test]
fn test_enforce_wins_over_audit() { ... }
#[test]
fn test_higher_priority_wins() { ... }
#[test]
fn test_multiple_enforces_highest_priority_message() { ... }- Create
Decisionenum inmodels/mod.rs - Values:
Allowed,Blocked,Warned,Audited - Implement
Serializefor JSON output - Add to log entries
- Implement
FromStrfor CLI parsing
Code:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Decision {
Allowed,
Blocked,
Warned,
Audited,
}- Add
mode: Option<PolicyMode>field - Add
priority: Option<i32>field - Add
decision: Option<Decision>field - Add
governance: Option<GovernanceMetadata>field - Add
trust_level: Option<TrustLevel>field - Use
#[serde(skip_serializing_if = "Option::is_none")]for all new fields - Verify existing log parsing still works
- Populate new fields when writing log entries
- Include mode from matched rule
- Include priority from matched rule
- Include decision from action result
- Include governance metadata if present
- Include trust level from run action
- Tests pass (68 unit + integration tests)
- Extend
cch logsto filter by mode - Extend
cch logsto filter by decision - Add
--mode <mode>flag - Add
--decision <decision>flag - Update help text and display columns
- Display mode (with default indicator)
- Display priority (with default indicator)
- Display full governance metadata block
- Display trust level for run actions
- Format output for readability
- Add
--jsonflag for structured output
Output Format:
Rule: <name>
Event: <hook_event_name>
Mode: <mode> (default: enforce)
Priority: <priority> (default: 0)
Matchers:
tools: [...]
extensions: [...]
...
Action:
<action_type>: <action_config>
Metadata:
author: <author>
created_by: <created_by>
reason: <reason>
...
- Parse recent log entries for the rule
- Count total triggers
- Count blocks/warns/audits/allowed
- Find last trigger timestamp
- Display in
cch explain ruleoutput - Add
--no-statsflag to skip log parsing
Activity Section:
Recent Activity:
Triggered: 14 times
Blocked: 3 times
Warned: 2 times
Audited: 9 times
Allowed: 0 times
Last trigger: 2025-01-20 14:32
- Output complete rule as JSON
- Include governance metadata
- Include activity stats
- Machine-parseable format with serde_json
- Document
modefield via CLI arg help - Document
priorityfield via CLI arg help - Added
cch explain rulescommand to list all rules - Added subcommand structure (rule, rules, event)
- Extend
runaction to support object format viaRunActionenum - Add optional
trustfield:local | verified | untrusted - Maintain backward compatibility with string format
- Parse both formats correctly using
#[serde(untagged)]
YAML Formats:
# Simple format (existing)
actions:
run: .claude/validators/check.py
# Extended format (new)
actions:
run:
script: .claude/validators/check.py
trust: local- Values:
Local,Verified,Untrusted - Implement Serialize/Deserialize
- Default:
Local(via #[default] derive)
- Include trust level in log entries when present
- Display in
cch explain ruleoutput - No enforcement (informational only in v1.1)
- Code documentation via doc comments
- Displayed in
cch explain ruleoutput - Note: Enforcement planned for future version (in doc comments)
- Code complete and compiles
- Unit tests written and passing (68 tests)
- Integration tests pass (all existing tests)
- Backward compatibility verified (v1.0 configs still work)
- Code documentation via doc comments
- Pre-commit checks pass:
cd cch_cli && cargo fmt && cargo clippy --all-targets --all-features -- -D warnings && cargo test
- PolicyMode parsing (case-insensitive)
- RuleMetadata parsing (all optional fields)
- Confidence enum parsing
- Priority sorting
- Conflict resolution logic
- Decision enum serialization
- Rule with mode=enforce blocks correctly
- Rule with mode=warn injects warning, doesn't block
- Rule with mode=audit logs only
- Priority sorting affects rule order
- Conflict resolution with mixed modes
- Enhanced log entries contain new fields
-
cch explain ruledisplays all fields - Backward compatibility with v1.0 configs
- All new fields use
Option<T> - All new fields use
#[serde(skip_serializing_if = "Option::is_none")] - Default values preserve v1.0 behavior
- Existing configs parse without changes
- Existing log parsers ignore new fields
- Priority sorting is O(n log n), negligible for typical rule counts (<100)
- Metadata adds minimal memory overhead
- Mode checking is O(1)
- Log entry size increase is bounded (<2KB per entry)