data-olympus 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {data_olympus-0.3.0 → data_olympus-0.3.2}/CHANGELOG.md +36 -5
  2. {data_olympus-0.3.0 → data_olympus-0.3.2}/PKG-INFO +1 -1
  3. {data_olympus-0.3.0 → data_olympus-0.3.2}/pyproject.toml +1 -1
  4. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/config.py +10 -0
  5. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/rest_api.py +30 -13
  6. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/server.py +31 -10
  7. {data_olympus-0.3.0 → data_olympus-0.3.2}/.gitignore +0 -0
  8. {data_olympus-0.3.0 → data_olympus-0.3.2}/LICENSE +0 -0
  9. {data_olympus-0.3.0 → data_olympus-0.3.2}/NOTICE +0 -0
  10. {data_olympus-0.3.0 → data_olympus-0.3.2}/README.md +0 -0
  11. {data_olympus-0.3.0 → data_olympus-0.3.2}/SPEC.md +0 -0
  12. {data_olympus-0.3.0 → data_olympus-0.3.2}/benchmarks/README.md +0 -0
  13. {data_olympus-0.3.0 → data_olympus-0.3.2}/bin/_kb_detect_workspace.sh +0 -0
  14. {data_olympus-0.3.0 → data_olympus-0.3.2}/bin/_kb_enforce.py +0 -0
  15. {data_olympus-0.3.0 → data_olympus-0.3.2}/bin/_kb_fallback.py +0 -0
  16. {data_olympus-0.3.0 → data_olympus-0.3.2}/bin/kb +0 -0
  17. {data_olympus-0.3.0 → data_olympus-0.3.2}/bin/kb-enforce-hook +0 -0
  18. {data_olympus-0.3.0 → data_olympus-0.3.2}/bin/opencode/data-olympus-gate.ts +0 -0
  19. {data_olympus-0.3.0 → data_olympus-0.3.2}/deploy/k8s/README.md +0 -0
  20. {data_olympus-0.3.0 → data_olympus-0.3.2}/deploy/k8s/read-replica/README.md +0 -0
  21. {data_olympus-0.3.0 → data_olympus-0.3.2}/docs/releases/README.md +0 -0
  22. {data_olympus-0.3.0 → data_olympus-0.3.2}/example-bundle/projects/acme-app/README.md +0 -0
  23. {data_olympus-0.3.0 → data_olympus-0.3.2}/example-bundle/projects/acme-app/components/api/README.md +0 -0
  24. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/__init__.py +0 -0
  25. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/audit_log.py +0 -0
  26. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/audit_trailers.py +0 -0
  27. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/auth.py +0 -0
  28. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/cli/__init__.py +0 -0
  29. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/cli/import_cmd.py +0 -0
  30. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/cli/indexgen.py +0 -0
  31. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/cli/main.py +0 -0
  32. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/cli/report_cmd.py +0 -0
  33. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/cooccurrence.py +0 -0
  34. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/dedup.py +0 -0
  35. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/durable.py +0 -0
  36. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/embeddings.py +0 -0
  37. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/enforce_policy.py +0 -0
  38. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/format/__init__.py +0 -0
  39. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/format/document.py +0 -0
  40. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/format/frontmatter.py +0 -0
  41. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/format/lint.py +0 -0
  42. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/format/validate.py +0 -0
  43. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/git_ops.py +0 -0
  44. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/health.py +0 -0
  45. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/__init__.py +0 -0
  46. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/adr.py +0 -0
  47. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/flat.py +0 -0
  48. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/model.py +0 -0
  49. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/okf.py +0 -0
  50. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/run.py +0 -0
  51. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/importer/stamp.py +0 -0
  52. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/index.py +0 -0
  53. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/markdown_parse.py +0 -0
  54. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/models.py +0 -0
  55. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/onboarding.py +0 -0
  56. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/onboarding_inflight.py +0 -0
  57. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/onboarding_playbook.py +0 -0
  58. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/pending.py +0 -0
  59. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/principals.py +0 -0
  60. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/prompts.py +0 -0
  61. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/push_queue.py +0 -0
  62. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/query_expansion.py +0 -0
  63. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/rate_limit.py +0 -0
  64. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/refresh.py +0 -0
  65. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/report.py +0 -0
  66. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/safe_id.py +0 -0
  67. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/search_gate.py +0 -0
  68. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/search_shortcut.py +0 -0
  69. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/session_metrics.py +0 -0
  70. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/setup_wizard.py +0 -0
  71. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/thin_pointer.py +0 -0
  72. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/tools_audit.py +0 -0
  73. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/tools_enforce.py +0 -0
  74. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/tools_onboarding.py +0 -0
  75. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/tools_read.py +0 -0
  76. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/tools_write.py +0 -0
  77. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/trigram.py +0 -0
  78. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/viewer/__init__.py +0 -0
  79. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/viewer/generator.py +0 -0
  80. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/worktrees.py +0 -0
  81. {data_olympus-0.3.0 → data_olympus-0.3.2}/src/data_olympus/write_gate.py +0 -0
@@ -12,6 +12,34 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.3.2] - 2026-07-05
16
+
17
+ ### Fixed
18
+
19
+ - **`gate/check` no longer self-DoSes multi-agent fleets with 429s.** The
20
+ enforcement hook calls `POST /api/v1/gate/check` (and `kb_gate_check`) once per
21
+ gated tool action for every agent, but it shared the write/consult quota
22
+ (`KB_RATE_LIMIT_PER_HOUR`, default 100). Behind an ingress with no auth, all
23
+ clients collapse to a single limiter bucket, so a few active agents exhausted
24
+ the hourly quota in minutes and everything after got `429 Too Many Requests`.
25
+ gate-check now has its own ceiling, `KB_GATE_CHECK_RATE_LIMIT_PER_HOUR`,
26
+ defaulting to `0` (unthrottled, consistent with the read routes) since it is a
27
+ mandatory per-action probe that does only cheap classification and no writes.
28
+ Set a positive value for an explicit backstop. `consult` and
29
+ `onboarding/cleanup-plan` stay throttled by `KB_RATE_LIMIT_PER_HOUR`.
30
+
31
+ ## [0.3.1] - 2026-07-05
32
+
33
+ ### Fixed
34
+
35
+ - **`prepare-git` initContainer is now idempotent across pod restarts.** It staged
36
+ the deploy key with `cp /etc/git-key-mount /state/git-key` then `chmod 0400`.
37
+ Because `/state` is a PVC, on any pod restart the `0400` (unwritable) key already
38
+ exists and the plain `cp` fails under `set -e`, leaving the pod stuck in
39
+ `Init:Error`. The manifest now `rm -f`s the staged key before copying (the
40
+ `/state` dir is writable to uid 65534 via `fsGroup`). Only the first boot was
41
+ ever exercised before; a `kubectl rollout restart` or any pod recreation hit it.
42
+
15
43
  ## [0.3.0] - 2026-07-03
16
44
 
17
45
  ### Added
@@ -419,10 +447,11 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
419
447
 
420
448
  - **Release delivery for 0.3.0.** Two release-pipeline bugs surfaced on the first
421
449
  0.3.0 run and are fixed so the tag, image, and PyPI package publish together: the
422
- Docker image build now copies `README.md`, `LICENSE`, and `NOTICE` (referenced by
423
- `pyproject.toml`) into the build context before the project-installing `uv sync`;
424
- and the PyPI publish runs inline in `tag-release.yml` rather than through a
425
- reusable workflow, which PyPI trusted publishing does not reliably match through.
450
+ Docker image build now copies everything `pyproject.toml` references into the
451
+ build context before the project-installing `uv sync` (`README.md`, `LICENSE`,
452
+ `NOTICE`, and the `bin/` machinery force-included into the wheel); and the PyPI
453
+ publish runs inline in `tag-release.yml` rather than through a reusable workflow,
454
+ which PyPI trusted publishing does not reliably match through.
426
455
  - **Stale auto-commit path locks are reclaimed.** A hard kill while an auto-commit
427
456
  held a per-path advisory lock left the lock on the `/state` volume with no
428
457
  in-process holder to release it, wedging that path (`rejected_path_lock_busy`)
@@ -1000,7 +1029,9 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1000
1029
  - `docs/adoption.md`: bring-your-own-KB guide (author, lint, index, serve, wire an agent).
1001
1030
  - `docs/comparison.md`: how data-olympus relates to OKF, enterprise catalogs, markdown KB tools, agent-context conventions, RAG, and ADR tooling.
1002
1031
 
1003
- [Unreleased]: https://github.com/knaisoma/data-olympus/compare/v0.3.0...HEAD
1032
+ [Unreleased]: https://github.com/knaisoma/data-olympus/compare/v0.3.2...HEAD
1033
+ [0.3.2]: https://github.com/knaisoma/data-olympus/compare/v0.3.1...v0.3.2
1034
+ [0.3.1]: https://github.com/knaisoma/data-olympus/compare/v0.3.0...v0.3.1
1004
1035
  [0.3.0]: https://github.com/knaisoma/data-olympus/compare/v0.2.0...v0.3.0
1005
1036
  [0.2.0]: https://github.com/knaisoma/data-olympus/compare/v0.1.1...v0.2.0
1006
1037
  [0.1.1]: https://github.com/knaisoma/data-olympus/compare/v0.1.0...v0.1.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-olympus
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Governance-grade knowledge-base format (OKF-compatible) plus CLI and single-writer MCP server
5
5
  Project-URL: Homepage, https://github.com/knaisoma/data-olympus
6
6
  Project-URL: Repository, https://github.com/knaisoma/data-olympus
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "data-olympus"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Governance-grade knowledge-base format (OKF-compatible) plus CLI and single-writer MCP server"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -28,6 +28,14 @@ class Config:
28
28
  write_block_paths: list[str] = field(default_factory=list)
29
29
  rate_limit_per_hour: int = 100
30
30
  rate_limit_per_ip_per_hour: int = 0
31
+ # Separate ceiling for the high-frequency /api/v1/gate/check route (the
32
+ # enforcement hook's per-tool-action freshness probe). 0 (default) disables
33
+ # throttling for that route: it is a mandatory, once-per-tool-call, read-only
34
+ # probe, so any fixed hourly quota self-DoSes an active multi-agent fleet
35
+ # (behind ingress all clients collapse to one limiter bucket). Reads are
36
+ # already unthrottled; this keeps gate/check consistent with them. Set a
37
+ # positive value only if you want an explicit backstop.
38
+ gate_check_rate_limit_per_hour: int = 0
31
39
  max_text_bytes: int = 262144
32
40
  max_postimage_bytes: int = 1048576
33
41
  max_body_bytes: int = 2097152
@@ -155,6 +163,7 @@ def load_config() -> Config:
155
163
  write_block_paths = _split_csv(os.getenv("KB_WRITE_BLOCK_PATHS", ""))
156
164
  rate_limit_per_hour = int(os.getenv("KB_RATE_LIMIT_PER_HOUR", "100"))
157
165
  rate_limit_per_ip_per_hour = int(os.getenv("KB_RATE_LIMIT_PER_IP_PER_HOUR", "0"))
166
+ gate_check_rate_limit_per_hour = int(os.getenv("KB_GATE_CHECK_RATE_LIMIT_PER_HOUR", "0"))
158
167
  max_text_bytes = int(os.getenv("KB_MAX_TEXT_BYTES", "262144"))
159
168
  max_postimage_bytes = int(os.getenv("KB_MAX_POSTIMAGE_BYTES", "1048576"))
160
169
  max_body_bytes = int(os.getenv("KB_MAX_BODY_BYTES", "2097152"))
@@ -226,6 +235,7 @@ def load_config() -> Config:
226
235
  write_block_paths=write_block_paths,
227
236
  rate_limit_per_hour=rate_limit_per_hour,
228
237
  rate_limit_per_ip_per_hour=rate_limit_per_ip_per_hour,
238
+ gate_check_rate_limit_per_hour=gate_check_rate_limit_per_hour,
229
239
  max_text_bytes=max_text_bytes,
230
240
  max_postimage_bytes=max_postimage_bytes,
231
241
  max_body_bytes=max_body_bytes,
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
32
32
  from starlette.requests import Request
33
33
 
34
34
  from data_olympus.models import HealthResponse
35
+ from data_olympus.rate_limit import SlidingWindowLimiter
35
36
  from data_olympus.server import ServerState
36
37
 
37
38
 
@@ -285,22 +286,33 @@ async def _read_json_capped(
285
286
  )
286
287
 
287
288
 
289
+ _USE_WRITE_LIMITER: Any = object()
290
+
291
+
288
292
  def _rate_limited(
289
293
  state: ServerState, request: Request, principal: Principal,
294
+ *, limiter: SlidingWindowLimiter | None = _USE_WRITE_LIMITER,
290
295
  ) -> JSONResponse | None:
291
- """Apply the shared sliding-window limiter to an enforcement-plane route.
292
-
293
- The consult / gate / cleanup-plan routes were previously unthrottled (item 6),
294
- so an anonymous or authenticated caller could hammer the classifier and ledger
295
- without bound. Reuse the same limiter the write routes use, keyed by
296
- (remote_addr, principal name) so a per-principal quota applies. Returns a 429
297
- JSONResponse when over quota, else None.
298
-
299
- When the limiter is absent (a read-only deployment with no write pipeline
300
- configured) throttling is skipped: there is no shared limiter object to
301
- consult and these routes are read-mostly there.
296
+ """Apply a sliding-window limiter to an enforcement-plane route.
297
+
298
+ The consult / cleanup-plan routes were previously unthrottled (item 6), so an
299
+ anonymous or authenticated caller could hammer the classifier and ledger
300
+ without bound. They reuse the write limiter (``state.rate_limiter``), keyed by
301
+ (remote_addr, principal name). Returns a 429 JSONResponse when over quota, else
302
+ None.
303
+
304
+ ``limiter`` overrides which limiter to consult. The high-frequency
305
+ /api/v1/gate/check route passes ``state.gate_rate_limiter`` (None by default),
306
+ so it is unthrottled unless an operator sets an explicit ceiling: it is a
307
+ mandatory once-per-tool-action probe, and any fixed quota shared across a
308
+ fleet (all clients collapse to one bucket behind ingress) self-DoSes with 429s.
309
+
310
+ When the resolved limiter is absent (a read-only deployment, or gate/check with
311
+ no ceiling configured) throttling is skipped.
302
312
  """
303
- limiter = state.rate_limiter
313
+ # Distinguish "no limiter arg" (use the write limiter) from an explicit None
314
+ # (the gate-check limiter with no ceiling: throttling is genuinely off).
315
+ limiter = state.rate_limiter if limiter is _USE_WRITE_LIMITER else limiter
304
316
  if limiter is None:
305
317
  return None
306
318
  remote_addr = request.client.host if request.client else "unknown"
@@ -662,7 +674,12 @@ def register_routes(
662
674
  principal, denied = _authorize(request, registry)
663
675
  if denied is not None:
664
676
  return denied
665
- if (throttled := _rate_limited(state, request, principal)) is not None:
677
+ # gate/check uses its own limiter (unthrottled by default): it is the
678
+ # hook's once-per-tool-action probe, so it must not share the write /
679
+ # consult quota (that self-DoSes a fleet, see _rate_limited).
680
+ if (throttled := _rate_limited(
681
+ state, request, principal, limiter=state.gate_rate_limiter
682
+ )) is not None:
666
683
  return throttled
667
684
  import time as _time
668
685
 
@@ -123,6 +123,7 @@ class ServerState:
123
123
  push_queue: PushQueue | None = None,
124
124
  pending: PendingQueue | None = None,
125
125
  rate_limiter: SlidingWindowLimiter | None = None,
126
+ gate_rate_limiter: SlidingWindowLimiter | None = None,
126
127
  blocklist: PathBlocklist | None = None,
127
128
  audit_log: AuditLog | None = None,
128
129
  classifier: IntentClassifier | None = None,
@@ -147,6 +148,10 @@ class ServerState:
147
148
  self.push_queue: PushQueue | None = push_queue
148
149
  self.pending: PendingQueue | None = pending
149
150
  self.rate_limiter: SlidingWindowLimiter | None = rate_limiter
151
+ # Separate limiter for /api/v1/gate/check (None = unthrottled, the default).
152
+ # Kept distinct from `rate_limiter` so gate-check traffic and the write /
153
+ # consult / cleanup-plan quota never share a bucket.
154
+ self.gate_rate_limiter: SlidingWindowLimiter | None = gate_rate_limiter
150
155
  self.blocklist: PathBlocklist | None = blocklist
151
156
  self.audit_log: AuditLog | None = audit_log
152
157
  # Process-wide write serializer (scope item 1): one instance shared by
@@ -232,6 +237,7 @@ def build_app(
232
237
  confidence_threshold: float = 0.85,
233
238
  rate_limit_per_hour: int = 100,
234
239
  rate_limit_per_ip_per_hour: int = 0,
240
+ gate_check_rate_limit_per_hour: int = 0,
235
241
  max_text_bytes: int = 262144,
236
242
  max_postimage_bytes: int = 1048576,
237
243
  max_body_bytes: int = 2097152,
@@ -279,6 +285,7 @@ def build_app(
279
285
  write_block_paths=write_block_paths or [],
280
286
  rate_limit_per_hour=rate_limit_per_hour,
281
287
  rate_limit_per_ip_per_hour=rate_limit_per_ip_per_hour,
288
+ gate_check_rate_limit_per_hour=gate_check_rate_limit_per_hour,
282
289
  max_text_bytes=max_text_bytes,
283
290
  max_postimage_bytes=max_postimage_bytes,
284
291
  max_body_bytes=max_body_bytes,
@@ -393,6 +400,13 @@ def build_app(
393
400
  max_per_hour=config.rate_limit_per_hour,
394
401
  max_per_ip_per_hour=config.rate_limit_per_ip_per_hour,
395
402
  )
403
+ # gate/check gets its own limiter only when a positive ceiling is set;
404
+ # 0 (default) leaves it unthrottled (see Config.gate_check_rate_limit_per_hour).
405
+ gate_rate_limiter = (
406
+ SlidingWindowLimiter(max_per_hour=config.gate_check_rate_limit_per_hour)
407
+ if config.gate_check_rate_limit_per_hour > 0
408
+ else None
409
+ )
396
410
  blocklist = PathBlocklist(
397
411
  tier_blocks=config.write_block_tiers,
398
412
  path_blocks=config.write_block_paths,
@@ -401,6 +415,7 @@ def build_app(
401
415
  state.push_queue = push_queue
402
416
  state.pending = pending
403
417
  state.rate_limiter = rate_limiter
418
+ state.gate_rate_limiter = gate_rate_limiter
404
419
  state.blocklist = blocklist
405
420
 
406
421
  if config.kb_remote_url and not config.read_only:
@@ -545,17 +560,22 @@ def build_app(
545
560
  )
546
561
  return resp.model_dump()
547
562
 
548
- def _mcp_rate_limited() -> dict[str, object] | None:
549
- """Apply the shared sliding-window limiter to an MCP enforcement-plane tool
563
+ def _mcp_rate_limited(*, gate: bool = False) -> dict[str, object] | None:
564
+ """Apply a sliding-window limiter to an MCP enforcement-plane tool
550
565
  (WP0b item (b)).
551
566
 
552
- The REST consult / gate-check / cleanup-plan routes throttle via
553
- ``_rate_limited``; the MCP tool paths did not, so an agent could hammer the
554
- classifier/ledger unbounded. Key on the resolved principal (there is no
555
- per-request remote addr on the MCP transport, so the limiter's per-agent
556
- quota is the meaningful dimension). Returns a rejection dict when over
557
- quota, else None. Skipped when no limiter is wired (read-only deploy)."""
558
- limiter = state.rate_limiter
567
+ The REST consult / cleanup-plan routes throttle via ``_rate_limited``; the
568
+ MCP tool paths did not, so an agent could hammer the classifier/ledger
569
+ unbounded. Key on the resolved principal (there is no per-request remote
570
+ addr on the MCP transport, so the limiter's per-agent quota is the
571
+ meaningful dimension). Returns a rejection dict when over quota, else None.
572
+
573
+ ``gate=True`` (kb_gate_check) consults ``state.gate_rate_limiter`` instead
574
+ of the write limiter; that limiter is None by default, so the
575
+ high-frequency freshness probe is unthrottled unless explicitly capped,
576
+ matching the REST /api/v1/gate/check route. Skipped when the resolved
577
+ limiter is None (read-only deploy, or gate-check with no ceiling set)."""
578
+ limiter = state.gate_rate_limiter if gate else state.rate_limiter
559
579
  if limiter is None:
560
580
  return None
561
581
  principal_name = _current_principal.get().name
@@ -768,7 +788,7 @@ def build_app(
768
788
  ) -> dict[str, object]:
769
789
  """Return a verdict (allow | consult_required) for a pending code action.
770
790
  Governed actions require a fresh consultation on record."""
771
- if (throttled := _mcp_rate_limited()) is not None:
791
+ if (throttled := _mcp_rate_limited(gate=True)) is not None:
772
792
  return throttled
773
793
  import time as _time
774
794
 
@@ -852,6 +872,7 @@ def build_app_from_config(config: Config, *, bootstrap_now: bool = True) -> Fast
852
872
  confidence_threshold=config.confidence_threshold,
853
873
  rate_limit_per_hour=config.rate_limit_per_hour,
854
874
  rate_limit_per_ip_per_hour=config.rate_limit_per_ip_per_hour,
875
+ gate_check_rate_limit_per_hour=config.gate_check_rate_limit_per_hour,
855
876
  max_text_bytes=config.max_text_bytes,
856
877
  max_postimage_bytes=config.max_postimage_bytes,
857
878
  max_body_bytes=config.max_body_bytes,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes