HANDOFF: permissions

Authority and status
- Only target project permissions is authoritative. Ignore all archived projects.
- Updates 1–5 have been applied in order. Superseded settings are discarded.
- No next update or later questions are available. Future updates replace only named keys.

CURRENT choices
{
  "deny_wins": false,
  "expiry_inclusive": false,
  "inherit_roles": false,
  "owner_bypass": false,
  "action_casefold": false,
  "group_union": false,
  "resource_scope": "prefix",
  "suspended_denies": false,
  "token_ttl": 200,
  "wildcard_action": false,
  "audit_denials": true,
  "default_allow": true
}

Fixed interface
Implement evaluate(payload) returning:
{allowed: boolean, reason: string, matched: [grant_index], audit: [reason]}

Subject fields: tenant, roles[], groups[], owner, suspended.
Resource fields: tenant, id.
Payload includes subject, resource, now, issued_at, action, grants, role_parents.
Inputs are ordinary ASCII. Do not mutate inputs.

Evaluation under CURRENT choices
1. Different subject/resource tenants: deny with reason "tenant".
2. Otherwise, now - issued_at > 200: deny with reason "token".
   Exactly 200 is not denied by this check.
3. Suspension does not trigger denial; suspended_denies is false.
4. Ownership does not bypass evaluation; owner_bypass is false.
Early returns have matched=[].

Use only subject.roles as effective roles. Do not union groups or follow role_parents.

A grant matches only when all conditions hold:
- Its role is in the effective roles.
- Its action equals the requested action, case-sensitively.
  "*" has no wildcard behavior; it can still match a literal requested "*".
- Its scope is a prefix of resource.id.
- Its expires is null, or now < expires.
  At now == expires, the grant is expired.

Gather EVERY matching grant's original index, retaining original grant order. Do not stop at the first allow or deny.

If no grants match:
- allowed=true, reason="default", matched=[].

If grants match:
- reason="grants".
- allowed is true iff any matching grant is an allow.
- Matching denies do not override a matching allow.
- If matching grants contain no allow, deny.
- Default allow does not apply when grants match.

For every result:
- If denied, audit=[reason].
- If allowed, audit=[].
This includes early tenant/token denials.

Retained fixed contract for possible future updates
- Early-check order is tenant, token, enabled suspension denial, enabled owner bypass.
- If group_union becomes true, add subject.groups to initial roles.
- If inherit_roles becomes true, follow role_parents transitively and tolerate cycles.
- If action_casefold becomes true, compare actions ASCII case-insensitively.
- If wildcard_action becomes true, grant action "*" matches any action.
- resource_scope="exact" requires scope == resource.id; "prefix" requires scope to prefix resource.id.
- expiry_inclusive=true permits now <= expires; false requires now < expires. Null expires always passes.
- deny_wins=true requires at least one matching allow and no matching deny; false requires any matching allow.
- default_allow governs only the no-match result.
- audit_denials enables [reason] only for denied results.
- All early returns, including owner allowance if enabled later, have matched=[].

Unresolved items
- No unresolved configuration choices or pending decisions were recorded.
- The supplied contract does not explicitly name the grant field/encoding that distinguishes allow from deny. Do not invent an authoritative schema; clarify if needed before implementation.