Step 595: add approval escalation planner

This commit is contained in:
Bill
2026-02-17 11:38:29 -07:00
parent d9bffde839
commit b0d72af656
4 changed files with 259 additions and 0 deletions

View File

@@ -4234,4 +4234,13 @@ target_link_libraries(step594_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step595_test tests/step595_test.cpp)
target_include_directories(step595_test PRIVATE src)
target_link_libraries(step595_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -0,0 +1,68 @@
#pragma once
// Step 595: Approval Escalation Planner
#include "PolicyGuardrailCatalog.h"
#include <string>
#include <vector>
enum class EscalationLevel {
None,
Reviewer,
Admin
};
struct EscalationPlan {
std::string operation;
GuardrailDecision decision = GuardrailDecision::RequireReview;
EscalationLevel level = EscalationLevel::Reviewer;
bool requiresJustification = true;
std::vector<std::string> approverRoles;
std::string reason;
};
class ApprovalEscalationPlanner {
public:
static EscalationPlan planFor(const PolicyGuardrailCatalog& catalog,
const std::string& operation) {
EscalationPlan plan;
plan.operation = operation;
const auto decision = catalog.evaluate(operation);
plan.decision = decision.decision;
plan.reason = decision.reason;
if (decision.decision == GuardrailDecision::Allow) {
plan.level = EscalationLevel::None;
plan.requiresJustification = false;
plan.approverRoles = {};
return plan;
}
if (decision.decision == GuardrailDecision::Deny) {
plan.level = EscalationLevel::Admin;
plan.requiresJustification = true;
plan.approverRoles = {"security-admin", "platform-owner"};
return plan;
}
plan.level = EscalationLevel::Reviewer;
plan.requiresJustification = true;
plan.approverRoles = {"reviewer"};
return plan;
}
static bool isExecutable(const EscalationPlan& plan) {
if (plan.decision == GuardrailDecision::Deny) return false;
return plan.level == EscalationLevel::None ||
plan.level == EscalationLevel::Reviewer;
}
static int riskScore(const EscalationPlan& plan) {
int score = 20;
if (plan.decision == GuardrailDecision::RequireReview) score += 30;
if (plan.decision == GuardrailDecision::Deny) score += 60;
if (plan.requiresJustification) score += 10;
if (plan.level == EscalationLevel::Admin) score += 10;
if (score > 100) score = 100;
return score;
}
};

View File

@@ -0,0 +1,149 @@
// Step 595: Approval Escalation Planner (12 tests)
#include "ApprovalEscalationPlanner.h"
#include <iostream>
static int passed = 0, failed = 0;
#define TEST(name) { std::cout << " " << #name << "... "; }
#define PASS() { std::cout << "PASS\n"; ++passed; }
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
static PolicyGuardrailCatalog catalogWithRules() {
PolicyGuardrailCatalog c;
std::string error;
c.addRule({"allow-push", "git push", GuardrailDecision::Allow, "trusted push path"}, &error);
c.addRule({"review-deploy", "deploy", GuardrailDecision::RequireReview, "deployment review required"}, &error);
c.addRule({"deny-rm", "rm -rf", GuardrailDecision::Deny, "destructive operation denied"}, &error);
return c;
}
void test_allow_rule_produces_no_escalation() {
TEST(allow_rule_produces_no_escalation);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "git push origin main");
CHECK(plan.level == EscalationLevel::None, "level should be none");
CHECK(!plan.requiresJustification, "justification should be false");
CHECK(plan.approverRoles.empty(), "approvers should be empty");
PASS();
}
void test_review_rule_produces_reviewer_escalation() {
TEST(review_rule_produces_reviewer_escalation);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "deploy prod");
CHECK(plan.level == EscalationLevel::Reviewer, "level should be reviewer");
CHECK(plan.requiresJustification, "justification should be true");
CHECK(plan.approverRoles.size() == 1 && plan.approverRoles[0] == "reviewer", "reviewer role expected");
PASS();
}
void test_deny_rule_produces_admin_escalation() {
TEST(deny_rule_produces_admin_escalation);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "rm -rf /tmp/x");
CHECK(plan.level == EscalationLevel::Admin, "level should be admin");
CHECK(plan.approverRoles.size() == 2, "two admin approvers expected");
PASS();
}
void test_default_review_when_no_rule_matches() {
TEST(default_review_when_no_rule_matches);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "unknown op");
CHECK(plan.decision == GuardrailDecision::RequireReview, "default decision should be review");
CHECK(plan.reason == "default_review_required", "default reason mismatch");
PASS();
}
void test_reason_propagates_from_guardrail_decision() {
TEST(reason_propagates_from_guardrail_decision);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "deploy prod");
CHECK(plan.reason == "deployment review required", "reason should propagate");
PASS();
}
void test_operation_field_is_preserved() {
TEST(operation_field_is_preserved);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "git push origin main");
CHECK(plan.operation == "git push origin main", "operation field mismatch");
PASS();
}
void test_is_executable_true_for_allow() {
TEST(is_executable_true_for_allow);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "git push origin main");
CHECK(ApprovalEscalationPlanner::isExecutable(plan), "allow should be executable");
PASS();
}
void test_is_executable_true_for_review() {
TEST(is_executable_true_for_review);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "deploy prod");
CHECK(ApprovalEscalationPlanner::isExecutable(plan), "review should be executable");
PASS();
}
void test_is_executable_false_for_deny() {
TEST(is_executable_false_for_deny);
auto c = catalogWithRules();
auto plan = ApprovalEscalationPlanner::planFor(c, "rm -rf /tmp/x");
CHECK(!ApprovalEscalationPlanner::isExecutable(plan), "deny should not be executable");
PASS();
}
void test_risk_score_increases_by_decision_severity() {
TEST(risk_score_increases_by_decision_severity);
auto c = catalogWithRules();
auto allowPlan = ApprovalEscalationPlanner::planFor(c, "git push origin main");
auto reviewPlan = ApprovalEscalationPlanner::planFor(c, "deploy prod");
auto denyPlan = ApprovalEscalationPlanner::planFor(c, "rm -rf /tmp/x");
CHECK(ApprovalEscalationPlanner::riskScore(allowPlan) < ApprovalEscalationPlanner::riskScore(reviewPlan),
"review risk should exceed allow risk");
CHECK(ApprovalEscalationPlanner::riskScore(reviewPlan) < ApprovalEscalationPlanner::riskScore(denyPlan),
"deny risk should exceed review risk");
PASS();
}
void test_risk_score_capped_at_100() {
TEST(risk_score_capped_at_100);
EscalationPlan p;
p.decision = GuardrailDecision::Deny;
p.level = EscalationLevel::Admin;
p.requiresJustification = true;
CHECK(ApprovalEscalationPlanner::riskScore(p) <= 100, "risk score should be capped");
PASS();
}
void test_admin_plan_requires_justification() {
TEST(admin_plan_requires_justification);
auto c = catalogWithRules();
auto p = ApprovalEscalationPlanner::planFor(c, "rm -rf /tmp/x");
CHECK(p.requiresJustification, "admin plan should require justification");
PASS();
}
int main() {
std::cout << "Step 595: Approval Escalation Planner\n";
test_allow_rule_produces_no_escalation(); // 1
test_review_rule_produces_reviewer_escalation(); // 2
test_deny_rule_produces_admin_escalation(); // 3
test_default_review_when_no_rule_matches(); // 4
test_reason_propagates_from_guardrail_decision(); // 5
test_operation_field_is_preserved(); // 6
test_is_executable_true_for_allow(); // 7
test_is_executable_true_for_review(); // 8
test_is_executable_false_for_deny(); // 9
test_risk_score_increases_by_decision_severity(); // 10
test_risk_score_capped_at_100(); // 11
test_admin_plan_requires_justification(); // 12
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -11374,3 +11374,36 @@ allow/review/deny decisions with explainable rule matching.
- `editor/src/PolicyGuardrailCatalog.h` within header-size limit (`75` <= `600`)
- `editor/tests/step594_test.cpp` within test-file size guidance (`158` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
### Step 595: Approval Escalation Planner
**Status:** PASS (12/12 tests)
Implements escalation planning from guardrail decisions, including approver
roles, execution eligibility, and normalized risk scoring.
**Files added:**
- `editor/src/ApprovalEscalationPlanner.h` - escalation planner module:
- converts allow/review/deny guardrail results into escalation plans
- assigns escalation level and approver roles
- computes execution eligibility from plan state
- computes bounded risk score (`0..100`)
- `editor/tests/step595_test.cpp` - 12 tests covering:
- allow/review/deny planning behavior
- default rule fallback behavior
- reason/operation propagation behavior
- execution eligibility behavior
- risk score ordering and cap behavior
**Files modified:**
- `editor/CMakeLists.txt` - `step595_test` target
**Verification run:**
- `cmake -S editor -B editor/build-native` - PASS
- `cmake --build editor/build-native --target step595_test step594_test` - PASS
- `./editor/build-native/step595_test` - PASS (12/12)
- `./editor/build-native/step594_test` - PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/ApprovalEscalationPlanner.h` within header-size limit (`68` <= `600`)
- `editor/tests/step595_test.cpp` within test-file size guidance (`149` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`