mr-668: drop actor_id from PolicyRequest; pass actor as separate arg

The MR-731 "server-authoritative actor identity" invariant was enforced
by an in-function chokepoint (`request.actor_id = actor.actor_id...`
overwrite inside `authorize_request`). That worked but relied on every
caller passing in a `PolicyRequest` and trusting the overwrite — a
comment-enforced invariant.

Move the invariant into the type system:

* `PolicyRequest` no longer carries `actor_id`. The struct now models
  what a caller wants to do, not who they are.
* `PolicyEngine::authorize(actor_id: &str, request: &PolicyRequest)`
  and `validate_request(actor_id, request)` take identity as a
  separate argument. The same shape `PolicyChecker::check` already had
  for the engine layer.
* `authorize_request` in the HTTP layer extracts `actor_id` from the
  bearer-resolved `ResolvedActor` and passes it positionally — no
  overwrite step that could be skipped.
* CLI `omnigraph policy explain` updated (the only other consumer
  that built a `PolicyRequest`).

Public API break for the `omnigraph-policy` crate. Worth it: handlers
can no longer accidentally populate `actor_id` from a request body
field, and external consumers are forced by the compiler to source
actor identity from a trusted path.

The MR-731 chokepoint test
`actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers`
still passes — the bearer-resolved actor is what reaches the engine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ragnor Comerford 2026-05-27 12:00:52 +02:00
parent 52f28cebe8
commit 76ee061cac
No known key found for this signature in database
3 changed files with 121 additions and 140 deletions

View file

@ -1333,12 +1333,12 @@ fn print_commit_human(commit: &CommitOutput) {
println!("created_at: {}", commit.created_at);
}
fn print_policy_explain(decision: &PolicyDecision, request: &PolicyRequest) {
fn print_policy_explain(decision: &PolicyDecision, actor_id: &str, request: &PolicyRequest) {
println!(
"decision: {}",
if decision.allowed { "allow" } else { "deny" }
);
println!("actor: {}", request.actor_id);
println!("actor: {}", actor_id);
println!("action: {}", request.action);
if let Some(branch) = &request.branch {
println!("branch: {}", branch);
@ -2471,13 +2471,12 @@ async fn main() -> Result<()> {
let config = load_cli_config(config.as_ref())?;
let engine = resolve_policy_engine(&config)?;
let request = PolicyRequest {
actor_id: actor,
action,
branch,
target_branch,
};
let decision = engine.authorize(&request)?;
print_policy_explain(&decision, &request);
let decision = engine.authorize(&actor, &request)?;
print_policy_explain(&decision, &actor, &request);
}
},
Command::Optimize {