W3 reasoning-quota wall — public port (ADR-245 Decision 8) - #245
Merged
Merged
Conversation
🛡️ GraQle PR Guardian💥 Blast Radius: 10 modules affected
Total blast radius: 10 🏛️ Governance Verdict
|
| Metric | Value |
|---|---|
| Blast Radius | 10 |
| Files Analyzed | 10 |
| Blocked | 0 |
| SHACL Violations | 0 |
| Verdict | WARN |
🔬 Powered by GraQle PR Guardian v0.1.0 · Scan completed 2026-07-30T15:42:02.225456+00:00
harishquantamix
previously approved these changes
Jul 30, 2026
The reasoning-FREQUENCY wall in the multi-wall lattice (independent of the node-cap SIZE wall). Free tier = 30 graq run/reason invocations per calendar month; paid unlimited. Turns "reason a lot for free forever" into a real upgrade trigger. - NEW graqle/licensing/reasoning_quota.py: ReasoningQuota + ReasoningQuotaExceeded. Tier from the VERIFIED manager.current_tier ONLY (a raw env/key never grants unlimited — CR-LIC-03b rule). Enforced ON by default (GRAQLE_ENFORCE_CAPS opt-OUT, shared with node-cap). internal=True EXEMPT (reasoning invoked by another metered action e.g. PR-Guardian never double-charges). FAIL-OPEN on any file/meter error — only ReasoningQuotaExceeded escapes (re-raised before the broad except). - main.py: _enforce_reasoning_quota() gates the 4 USER-value reasoning paths BEFORE areason runs: run(), bench() query loop, reason() single + batch. The 2 internal probes (bench smoke-test, safety_check) are correctly exempt. Sentinel graq_reason pass1 BLOCK (BLOCKER-1: 3 of 6 areason sites ungated) -> gated the user paths, classified the internal probes -> pass2 APPROVE 92% 0-BLOCKER (MAJOR-2 exception-escapes-fail-open + MAJOR-3 paid-tier fail-safe both verified). 17 tests (block-after-cap, paid-unlimited, internal-exempt, verified-tier-not-raw-env, fail-open, malformed-json, quota-escapes-fail-open). Zero regressions (pre-existing gate_install failures confirmed on clean master). Ships in the public wheel. Own version lane — NOT 0.81.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #316 put the quota wall in the CLI only, so the MCP graq_reason tool, the chat agent and api.py reached graph.areason() around it — free unlimited reasoning on a primary surface. The rule from that review: a wall at one surface is not a wall. This enforces at the primitive instead. - NEW graqle/licensing/reasoning_gate.py: one middleware, check_reasoning_quota(). internal=True is the ONLY exemption; ReasoningQuotaExceeded is re-raised before the broad except (fail-open on genuine meter faults, now logged at WARNING). - graph.areason() and graph.areason_stream() gate on entry. reason() delegates to areason (charged once); areason_batch() fans out per query (charged N). - graq debate gated: DebateOrchestrator.run() never reached areason. - reasoning_quota.py: cross-process file lock (msvcrt/fcntl) around the read-modify-write so racing runs cannot both spend the last unit. - CLI keeps only the upgrade-CTA wrapper; it no longer meters (no double-charge). - Benchmark runners + graq bench pass internal=True so evals never block a free-tier contributor. Sentinel: 3 passes on the full 75,773-node graph. Pass 1 BLOCK (3 blockers) -> pass 2 BLOCK (2 new) -> pass 3 APPROVE 91%, 0 blockers. BLOCKER-1 (unwalled areason_stream + debate) was real and is fixed. BLOCKER-2 (CI auto-exempt) was real: `export CI=true` was a one-line bypass — the exemption is removed entirely. BLOCKER-3/4/5 were refuted with code evidence and pinned as regression tests. Tests: 269 licensing pass, 100% statement coverage on both quota modules, 354 core pass, 521 CLI pass (10 pre-existing test_gate_install failures, identical set with and without this change). Five mutation experiments prove detection: removing either wall, disabling the lock (8/8 threads took the last unit), inverting the re-raise, and injecting internal=True at a server endpoint each fail the corresponding guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urity assertion Two defects in test_server_request_model_cannot_set_internal, both surfaced by rebasing W3 onto master's CR-010.R6: 1. sys.modules pollution. Importing a graqle.server module in-process leaves it loaded for the rest of the session, and tamper_evidence.verifier refuses to import when a server/studio module is present (moat-M2 _assert_isolated, WS-A3). One import in this file caused 11 unrelated failures in tests/test_cli/test_headless_contract.py when the suites ran together, while each file passed alone. Now probed in a subprocess. 2. The assertion never ran. It imported ReasonRequest from graqle.server.app, which does not define it (the models live in graqle.server.models), under a bare `except Exception: pytest.skip(...)`. The ImportError was swallowed, so this security check silently skipped on every machine. A skip-on-any-exception guard around an import is indistinguishable from a pass. Sentinel pass 1 raised two further blockers, both real and both fixed here: - Skip narrowed to ImportError (not ModuleNotFoundError). A genuinely-absent optional extra can surface as a plain ImportError — a transitive C-extension that fails to load, or a broken re-export — which would have hard-failed CI on a machine where the extra is simply not installed. - Interpreter guard added. CI runs the bare `pytest` shim (ci.yml:42), not `python -m pytest`, so sys.executable is not guaranteed to be the venv holding graqle. Without the guard the probe would exit 3 and SKIP, silently losing the assertion again — the very defect this rewrite fixes. It now fails loudly: a wrong interpreter is an environment bug, never a reason to drop coverage. Also widened to cover BatchReasonRequest, not just ReasonRequest. Mutation-tested twice: probing a field that DOES exist returns rc=1 (the assertion can fail), and an interpreter that cannot import graqle returns rc=1 (the guard fails loudly rather than skipping). Combined run 346 passed / 0 failed (was 12 failed); licensing 270 (up 1 — the un-skipped test); no server leak. W3 gates intact at graph.py:1899/2117 and R6 intact post-rebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quantamixsol
force-pushed
the
cr-mon-w3-reasoning-quota-public
branch
from
July 30, 2026 06:43
bb75efe to
4cc7963
Compare
…ne old months Pre-merge line-by-line review of this PR found three defects the process gates missed. Each is fixed and pinned with a mutation-tested regression test. F1 (BLOCKER) — GRAQLE_QUOTA_DIR was a self-attested one-line paywall bypass. resolve_quota_dir() honoured the env var unconditionally, so a FREE user at the cap who ran `export GRAQLE_QUOTA_DIR=$(mktemp -d)` got a fresh empty counter on every invocation: unlimited free reasoning. Proven by execution — the same check_reasoning_quota() call raised ReasoningQuotaExceeded before repointing the var and was ALLOWED after. This is categorically worse than the local-bypass class ADR-245 already accepts (deleting .graqle/reasoning_quota.json): it persists via a shell profile so it is set once and never repeated, it is non-destructive so nothing looks tampered with, and it needs no repeat action. It also contradicted this module's own rule — quota_exempt() refuses to honour CI=true precisely because self-attested env exemptions are banned (Decision 8 rule 3). An env var that relocates the counter is that same bypass wearing a different hat. Fix: the override is honoured only while PYTEST_CURRENT_TEST is set, i.e. inside a real pytest process. Tests can still redirect the meter; a production run cannot. Forging the marker is no longer a quiet, documented override. F2 (follow-up) — the quota file grew a month key forever; nothing pruned it. Now trimmed to 13 months on write. Non-month keys (schema_version) are preserved, and unparseable keys are left alone rather than silently discarded. F3 (follow-up) — peek() checked only the tier while check_and_record() also short-circuits on quota_enforcement_enabled(), so with GRAQLE_ENFORCE_CAPS=0 at the cap peek() said allowed=False while calls actually succeeded. Nothing user-facing consumes peek() today (its only caller is the exempt-path short-circuit), but the two must not drift. Mutation-tested: reverting each fix individually makes its test fail (verified all three). Bypass confirmed closed end-to-end: resolve_quota_dir() returns .graqle in a production run regardless of the env var. Suite 350 passed / 0 failed (was 346 + 4 new). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sentinel pass 1 blocked the previous commit and was right on both counts.
BLOCKER 1 — the fix was theatre. _under_pytest() keyed off PYTEST_CURRENT_TEST,
which is itself a self-attested env var: it replaced one bypass with another.
Proven forgeable:
PYTEST_CURRENT_TEST=forged::call GRAQLE_QUOTA_DIR=/tmp/x -> resolved /tmp/x
Now `"pytest" in sys.modules or "_pytest" in sys.modules`. An attacker cannot
import pytest into a process that never imported it, so sys.modules closes what
an env check cannot. Same attack now resolves to .graqle.
BLOCKER 2 — wrong timing, silent real-store pollution. PYTEST_CURRENT_TEST is
set per-test (setup/call/teardown) and is ABSENT during module import,
collection and session-scoped fixtures. Measured at import time:
PYTEST_CURRENT_TEST -> False, "pytest" in sys.modules -> True. So a test that
resolved the quota dir in any of those phases would have silently fallen through
to the developer's REAL ./.graqle and mutated their actual quota count.
sys.modules is true from process start, covering every phase uniformly.
The regression test now runs the production case in a SUBPROCESS (in-process,
pytest is necessarily loaded) and FORGES PYTEST_CURRENT_TEST inside it, pinning
the refutation so the weak guard cannot come back. The companion test asserts
the override still works with PYTEST_CURRENT_TEST deleted — proving the
collection-time path is covered.
Mutation-tested: restoring the env-var guard makes the forged-env test fail.
Suite 350 passed / 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
harishquantamix
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
W3 reasoning-quota wall — public port (ADR-245 Decision 8)
Ports the W3 reasoning-quota monetisation wall to the public SDK. This is a
cherry-pick of already-merged private work, not new design:
39d4bf48271d6ae588567ae3bb75efe3reasoning_quota.pyarrived as a modify/delete conflict when #321 was picked alone. Both are required in order.
What it does
A free monthly reasoning allowance. Over the cap, a FREE-tier user gets
ReasoningQuotaExceededand an upgrade CTA; reads and the graph keep working.Where the wall sits — the whole point of Decision 8
graqle/core/graph.py, three sites:areason()server/app.pyall funnel through itareason_stream()StreamingOrchestratorand never reachesareason()reason()(sync)areason(); deliberately NOTinternal=TruePlus
cli/commands/debate.py—DebateOrchestratoralso reasons outsideareason(),so it carries an explicit
check_reasoning_quota().Verification
private/masterfor all 5 implementation files(
reasoning_gate.py,reasoning_quota.py,debate.py, and both test files).areason_batch→self.areason(...)with nointernalarg ⇒ N-query batch = N units.graqle.licensing.*; zero references tograqle/cloud/*orgraqle/server/*, so there is no ImportError→fail-open path.import graqle.cli.mainsucceeds — no circular import.Pre-existing red — not from this diff
tests/test_cli/test_gate_install.pyfails 9 of 44 identically on the untouched basec51d1f26and on this branch. Verified by checking out the base and re-running.Unrelated to W3.
Governance — ADR-209
bolt,graph_health.degraded: falseghand base advancedplan_3859cba5graq_edit(strategy="literal")for docsgraq_reason×2 on the full diff (graq_reviewis DOWN). Pass 1: 5 BLOCKERs, all self-labelled unverified. Each reproduced against code and refuted with evidence. Pass 2: APPROVE, 0 blockers, 87%Rule #0 —
git remote -vrun before push. Public cherry-pick authorised onlybecause private #321 is merged (
mergedAt 2026-07-29T10:53:02Z, base advanced to65be4f75). Sequential gate satisfied. No version files in this diff; 0.83.0 releaseis a separate PR. Stray
(inferred from graph)file excluded.🤖 Generated with Claude Code
Update — rebased onto
21c91915+ a real regression fixedMaster gained CR-010.R6 (#244, scheduler contract), which also edits
graqle/core/graph.py. Rebased. GitHub reportedMERGEABLE, zero conflicts — R6'shunks are lines 33–1185, W3's are 1774–2020, no textual overlap.
But
MERGEABLEis notcorrect. Running the suites together after the rebasesurfaced 11–12 real failures that git could not have detected:
21c91915pytest tests/test_licensing/ tests/test_cli/test_headless_contract.pyEach file passed alone on both. Ordering-dependent pollution, introduced by W3's tests.
Root cause (traced to a real traceback, not inferred)
test_reasoning_gate.pyimportedgraqle.server.appin-process, leavinggraqle.serverinsys.modules.graqle/governance/tamper_evidence/verifier.py::_assert_isolated(moat-M2 / WS-A3) raises
ImportErrorwhen any server/studio module is alreadyloaded. R6's suite is the first to load the verifier after the licensing suite — so the
collision only appears once both features coexist.
Second, worse defect — found while fixing the first
That test imported
ReasonRequestfromgraqle.server.app, which does not defineit (the models live in
graqle.server.models), under a bareexcept Exception: pytest.skip(...). TheImportErrorwas swallowed, so this securityassertion — no HTTP request field may inject
internal=Trueand buy unmeteredreasoning — never executed on any machine, on private or public.
The fix (one file)
sys.modules.graqle.server.models; widened to coverBatchReasonRequesttoo.except ImportErroronly; anything else now fails.pytestshim (ci.yml:42), notpython -m pytest, sosys.executablemay not be the venv holding graqle. Without aguard the probe would exit 3 and skip — silently recreating the very defect this
rewrite fixes. It now asserts
import graqlesucceeds first and fails loudly.Evidence
rc=1(the assertion canfail); an interpreter that cannot import graqle →
rc=1(guard fails loud, never skips).graqle.server in sys.modulesafter the licensing suite →False.graph.py:1899/1901(areason) and:2117/2119(areason_stream);sync
reason()still ungated by design. R6 intact.test_gate_install.pyfails 10/608 identically on base and branch — pre-existing,proven by checking out the base and re-running.
Sentinel (
graq_reviewDOWN →graq_reason×2, full graph, non-degraded)Pass 1 BLOCKED with 2 blockers — both correct this time, and blocker 2's premise
was verified against the real CI config before fixing. Both fixed. Pass 2:
APPROVE, 0 blockers, 92%, unanimous.
Known red — infra, not this diff
Release Gate (PyPI):release_gate prediction provider failed: FileNotFoundError→ exit 2.Documented repo-wide flake (empty
GRAQLE_LICENSE, no graqle.json/Neo4j in CI); fails on8/8 branches including ones that merged fine. Needs an owner-approved infra fix.
Update 2 — independent pre-merge review found a paywall bypass
The earlier sentinel passes were process gates on a diff-in-progress. This is a fresh
line-by-line read of the whole change. It found 3 defects, each proven by execution.
🔴 F1 (BLOCKER) —
GRAQLE_QUOTA_DIRwas a one-line unlimited-reasoning bypassresolve_quota_dir()honoured the env var unconditionally, so the meter could bepointed anywhere. Proven: a FREE user at 30/30 gets
ReasoningQuotaExceeded; setGRAQLE_QUOTA_DIRto a fresh temp dir and the same call is allowed.Worse than the local-bypass class ADR-245 already accepts (deleting the quota file)
on three counts: it persists via a shell profile (set once, never repeated), it is
non-destructive (nothing looks tampered with), and it needs no repeat action.
It also contradicted this module's own rule —
quota_exempt()refuses to honourCI=trueprecisely because self-attested env exemptions are banned by Decision 8rule 3. An env var that relocates the counter is that same bypass wearing a different hat.
Fix: the override is honoured only when
"pytest" in sys.modules.F2 (follow-up) — unbounded file growth
data[month] = used + 1never pruned. Seeded 36 months → all 36 retained. Now trimmed to13 months on write;
schema_versionpreserved; unparseable keys left alone.F3 (follow-up) —
peek()disagreed withcheck_and_record()peek()checked only the tier. WithGRAQLE_ENFORCE_CAPS=0at the cap it reportedallowed=Falsewhile calls actually succeeded. Not user-visible today — its solecaller is
check_and_record's own exempt-path return (grep: 1 hit) — but the two mustnot drift.
Evidence
restoring it passes. Verified by editing the source back and re-running.
PYTEST_CURRENT_TESTresolves to.graqle.pass 2 APPROVE, 0 blockers, 93%.