Permission change · before release

PermitDiff

Detect when a permission change grants more authority.

Baseline policy

refund.create

allow

≤ $100

Candidate policy

refund.create

allow

≤ $100

Scenario · refund.create

$150

Reading the candidate ceiling.

File values: 10_000 → 50_000 minor units (100 minor units = $1 USD · $100 → $500). The crossed scenario is 15_000 ($150).

A text diff sees two numbers. PermitDiff sees that a refund was denied before and allowed after the policy change.

The check

One corpus watches observed behavior. A separate rule pass watches authority the corpus may miss.

A higher ceiling, a removed guard, and a reordered rule set are different policy changes. PermitDiff runs the same scenarios against baseline and candidate, then examines static authority when scenario coverage is insufficient.

Concrete transition

refund.create · $100 → $500

10,000 → 50,000 minor units · 100 minor units = $1 USD

One ceiling. Three scenarios. One authority expansion.

refund.create · $50

Baselineallow

Candidateallow

Unchanged

refund.create · $150

Baselinedeny

Candidateallow

Expansion

search.query · $0

Baselinedeny

Candidatedeny

Unchanged

The $150 scenario (15,000 minor units at 100 minor units = $1 USD) crosses the former ceiling. Static analysis independently records rule_scope_broadened with reason authority_expansion.

quickstart.py

PASS

Identical, or restriction only, after both channels.

REVIEW

ceiling example

An observed expansion, or a static sign of one.

BLOCK

reorder example

A changed rule cannot be shown to be non-expanding.

Measurement contract

What the public reference actually checks.

EVALUATION METHOD

A permission change needs a review when the same scenario is denied before and allowed after.

Public reference quickstart: refund.create ceiling 10_000 → 50_000 minor units ($100 → $500 at 100 minor units = $1 USD), scenarios at $50 (5_000), $150 (15_000), and search.query at 0.

01

$50 refund

allow on both policies
02

$150 refund

deny, then allow
03

search.query

deny on both policies
04

Static ceiling

widened allow bound, checked apart from the corpus
05

Public verdict

review · authority_expansion
06

Not claimed

runtime authorization, proof, production IAM, external validation

Validation

What the evidence actually supports.

Claims stay scoped

Authority transition

SYNTHETIC

The public quickstart shows refund.create at $150 (15,000 minor units) move from deny to allow when the ceiling rises from $100 to $500 (10,000 → 50,000 minor units; 100 minor units = $1 USD).

This is one runnable reference example, not an external benchmark.

External corpus

NOT CLAIMED

No external corpus result is claimed.

Local reference tests are not independent validation.

Engineering decisions

Constraints that keep an authority diff from becoming a decorative policy comparison.

01

Compare decisions, not text

The ceiling change is one number. The $150 refund is a new allow.

02

Keep the two channels separate

A scenario corpus can miss a rule. A static check can be incomplete. Neither one gets to hide the other.

03

Fail closed on an unknown change

If a changed rule cannot be shown to be non-expanding, the public verdict is block.

04

Make the result reproducible

The amounts on the case study are the amounts in examples/quickstart.py.

Forensic reorder

The corpus records a restriction. The reordered rules still cannot be proven non-expanding.

04_ambiguous_reorder.py

Baseline

  1. 01allow deploy.*
  2. 02deny deploy.prod

Candidate

  1. 01deny deploy.prod
  2. 02allow deploy.*
deploy.prod
allow → deny
Static finding
rule_order_changed
Verdict
BLOCK · fail_closed_unknown

Unknown semantics fail closed. Not a runtime authorizer · Not a security proof · Not production IAM · No external validation claimed

Provenance boundary

One product name. Two evidence surfaces.

Public reference

What you can run

  • Stdlib Python. A tool name plus an amount ceiling.
  • Verdicts are pass, review, and block.
  • No waiver ledger, SARIF output, or GitHub Action.

Private alpha

Not in that repository

  • A broader policy language and a release gate.
  • SARIF, CI, and exact expiring waivers.
  • Git baseline resolution on the development line, not in release v0.1.0rc2.
  • Development identity 0.1.0rc3.dev0. Latest release v0.1.0rc2.

A public REVIEW corresponds to a private gate failure until an exact waiver exists. This site does not link to the private repository.

Reproduction

pin bc9fa27
git clone https://github.com/TensorScholar/permitdiff-reference
cd permitdiff-reference
git checkout bc9fa27bde27caa21083604a91315e497e678199
python3 examples/quickstart.py
Recorded stdout · pinned checkout
decision: review
observed: {'same': 2, 'expanded': 1, 'restricted': 0, 'unknown': 0}
findings: ['rule_scope_broadened:refund-allow']
reasons: ['authority_expansion']

PermitDiff reference comparison
baseline:  baseline v0 digest=80c0a334ae89...
candidate: candidate v1 digest=6e93f3d9c299...
corpus: 3 scenarios digest=cb0a3af02493...

Summary:
  privilege_expansions: 1
  restrictions: 0
  unknown_transitions: 0
  new_allows: 1
  approval_bypasses: 0
  static_authority_expansions: 1
  static_authority_unknowns: 0

Transitions:
  [unchanged/note] corpus-000: refund.create amount=5000 unchanged (allow (rule refund-allow)); no review signal.
  [expanded/high] corpus-001: refund.create amount=15000 deny (default) -> allow (rule refund-allow).
  [unchanged/note] corpus-002: search.query amount=0 unchanged (deny (default)); no review signal.

Static findings:
  [potential_expansion] rule_scope_broadened: Rule 'refund-allow': allow scope broadened (match relation: broader).

Unknown semantics fail closed. See examples/README.md for the full suite.