TARGET PROJECT: permissions — HANDOFF NOTE

Purpose:
Implement evaluate(payload) -> {allowed:boolean, reason:string, matched:[grant_index], audit:[reason]}

Inputs:
- subject: tenant, roles[], groups[], owner, suspended
- resource: tenant, id
- payload: now, issued_at, action, grants, role_parents
- Inputs are ordinary ASCII. Do not mutate inputs.

Current configuration (all keys resolved; later updates superseded earlier values):
{
  "action_casefold": true,
  "audit_denials": false,
  "default_allow": false,
  "deny_wins": true,
  "expiry_inclusive": true,
  "group_union": true,
  "inherit_roles": true,
  "owner_bypass": true,
  "resource_scope": "exact",
  "suspended_denies": true,
  "token_ttl": 100,
  "wildcard_action": true
}

Algorithm contract, in order:
1. If subject tenant differs from resource tenant: deny, reason="tenant", matched=[].
2. Else if now - issued_at > token_ttl: deny, reason="token", matched=[].
3. Else if suspended_denies is enabled and subject is suspended: deny, reason="suspended", matched=[].
4. Else if owner_bypass is enabled and subject is owner: allow, reason="owner", matched=[].
5. Otherwise evaluate grants:
   - Initial roles = subject.roles, plus subject.groups because group_union=true.
   - Because inherit_roles=true, follow role_parents transitively; tolerate cycles.
   - A grant matches only if:
     * grant role is in the expanded roles;
     * grant action equals requested action ASCII case-insensitively because action_casefold=true, OR grant action="*" because wildcard_action=true;
     * grant scope equals resource.id because resource_scope="exact" (prefix matching does not apply);
     * grant expires is null, or now <= expires because expiry_inclusive=true.
   - Collect the original index of every matching grant, in grant order.
6. Decision after grant evaluation:
   - No matching grants: allowed = default_allow (currently false), reason="default", matched=[].
   - Matching grants: reason="grants". Because deny_wins=true, allow only if at least one matching grant is allow and no matching grant is deny; otherwise deny.
7. Audit:
   - audit = [reason] only when result is denied and audit_denials is enabled.
   - Current audit_denials=false, so audit=[] in all cases.

Unresolved items: none. All 12 configuration keys above are set to their current values.

Discarded: all archived/non-target project settings and any superseded target values (notably resource_scope="prefix", suspended_denies=false, action_casefold=false, group_union=false).