oc_chat
Loading...
Searching...
No Matches
resource_caps.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/resource_caps.py
4
5# OCT-LINT: disable=no_hardcoded_secrets reason="Module docstring prose trips Shannon entropy (5.05 bits/char, pre-existing at BASE); no secret (operator ruling 2026-09-05)"
6"""
7Normative resource caps — hard ceilings model args cannot exceed (M8.0 WU-9).
8
9Purpose
10-------
11Appendix B of the locked V1.0 spec (LD-3) fixes hard ceilings that bound the
12transcript-flood / provider-egress-cost / runaway-call DoS surface (F38/F52/F19;
13every turn re-ships the whole transcript, F72). A *model-supplied* argument can
14never exceed them: ``timeout``, ``max_bytes``/``max_chars``, ``max_results`` are
15clamped at the tool boundary; the per-turn tool-call count and the parallel-worker
16pool are bounded in the react-loop. The values are unified at **5 MB** for the
17read / fetch-body / output ceilings (LD-3). The per-turn tool-call ceiling is an
18**absolute ceiling a settings write may LOWER but never RAISE** (amended from the
19V1.0 Appendix-B 10 on operator approval). Plan 238 (CONFIG-01) moved every value
20below into the shipped, read-only ``oc_chat.defaults.toml``, hard-denied for
21writes by name in ``tools/_sandbox.py``: the names here are import-time aliases
22bound from :mod:`oc_chat.core.defaults`, and no value is written twice. The workspace
23``max_tool_calls_per_turn`` setting is a model-reachable, human-confirm-tier write,
24so F33/§5.4 bar it from raising a security ceiling; raising the ceiling above the
25default is done only via the operator-out-of-band env knob
26``OC_CHAT_MAX_TOOL_CALLS_CEILING`` (``_env_guard``-enrolled, stripped from a
27workspace ``.env``). The F52 "non-overridable by model args" DoS bound thus holds
28against the settings-file path too — see :func:`resolve_max_tool_calls_per_turn`.
29
30Responsibilities
31----------------
32- The cap constants (:data:`MAX_TOOL_CALLS_PER_TURN`, :data:`MAX_PARALLEL_WORKERS`,
33 :data:`MAX_TIMEOUT_S`, :data:`MAX_BYTES`, :data:`MAX_SEARCH_RESULTS`).
34- :func:`clamp_timeout` / :func:`clamp_bytes` / :func:`clamp_results` /
35 :func:`clamp_workers` — clamp a (possibly hostile) model value to its ceiling.
36- :func:`partition_call_budget` — split this iteration's tool calls into
37 (allowed, denied) given the per-turn budget already spent.
38- :func:`resolve_session_cost_hard_cap_usd` / :func:`resolve_cost_thresholds_usd`
39 — the plan-237 (CAPS-01) money bounds, resolved under the same F33/§5.4 rule.
40
41Contracts
42---------
43- A clamp NEVER returns a value above its ceiling; a non-int / unparseable value
44 fails to the ceiling (bounded), never to "unbounded".
45- Clamps only bound the UPPER side; a small/negative value passes through (a tiny
46 read/timeout is not a DoS).
47- :func:`partition_call_budget` is the deterministic per-turn cap helper; the
48 react-loop denies (does not execute) calls beyond the budget but still answers
49 them so the tool-call/result pairing stays well-formed.
50
51Dependencies
52------------
53- oc_diagnostics (structured debug logging via _dbg)
54- oc_chat.core.defaults (shipped cap values, ceilings, env-var names and setting keys)
55- oc_chat.core.settings (lazy; runtime_settings for workspace cap and checkpoint requests)
56
57Diagnostics
58-----------
59Domain: OC_CHAT.CORE
60Levels:
61 L2 — a model arg was clamped below its requested value
62 L3 — per-call clamp detail
63 L4 — deep tracing
64"""
65from __future__ import annotations
66
67from oc_diagnostics import _dbg
68
69import os
70
71from oc_chat.core.defaults import ceiling, env_var, setting_key, shipped
72
73#: Appendix B (LD-3) — normative hard ceilings. The VALUES live in the shipped,
74#: read-only ``oc_chat.defaults.toml`` (CONFIG-01, plan 238), which is hard-denied
75#: for writes by name; these names remain as import-time aliases so every existing
76#: call site keeps working.
77#: Per-turn tool-call ceiling DEFAULT (amended from the V1.0 Appendix-B value of
78#: 10 → 50, operator-approved 2026-06-13). This is also the default ABSOLUTE
79#: ceiling: a workspace ``max_tool_calls_per_turn`` setting may LOWER the effective
80#: cap below it but can NEVER RAISE it (the settings file is model-reachable;
81#: F33/§5.4). Raising the ceiling above the default is done only via the OOB env
82#: knob :data:`_CAP_CEILING_ENV_VAR` — see :func:`resolve_max_tool_calls_per_turn`.
83MAX_TOOL_CALLS_PER_TURN = shipped("tool_calls_per_turn")
84MAX_PARALLEL_WORKERS = shipped("parallel_workers")
85MAX_TIMEOUT_S = shipped("timeout_seconds")
86MAX_BYTES = shipped("byte_budget") # 5 MB read / fetch-body / output ceiling
87MAX_SEARCH_RESULTS = shipped("search_results")
88MAX_DIR_DEPTH = shipped("dir_depth") # directory-walk recursion depth (list_dir)
89
90#: §4.4(a) OOB env knob: raises the absolute per-turn-cap ceiling above the code
91#: default. Operator-out-of-band only — enrolled in ``_env_guard._AUTHORITY_EXACT``
92#: (stripped from a workspace ``.env``, preserved from the real process env),
93#: never settable from the workspace settings file. Absent / non-positive / non-int
94#: ⇒ the code default :data:`MAX_TOOL_CALLS_PER_TURN`. The name is declared by the
95#: ``tool_calls_per_turn`` table's ``env`` field, not restated here.
96_CAP_CEILING_ENV_VAR = env_var("tool_calls_per_turn")
97
98#: CAPS-01 (plan 237): the OOB env knob that raises the session dollar hard cap.
99#: Same class of authority as :data:`_CAP_CEILING_ENV_VAR` and enrolled in
100#: ``_env_guard._AUTHORITY_EXACT`` beside it. The name is declared by the
101#: ``session_cost_hard_cap_usd`` table's ``env`` field, not restated here — the
102#: refusal message reads it from here so the text can never name a stale knob.
103_COST_CEILING_ENV_VAR = env_var("session_cost_hard_cap_usd")
104
105
106def _clamp_upper(value: object, ceiling: int, *, label: str) -> int:
107 """Return ``int(value)`` bounded above by *ceiling*; an unparseable value fails
108 to *ceiling* (bounded, never unbounded). Values at or below the ceiling pass."""
109 func = "_clamp_upper"
110 try:
111 v = int(value) # type: ignore[arg-type]
112 except (TypeError, ValueError, OverflowError): # OverflowError: int(float('inf'))
113 _dbg("OC_CHAT.CORE", func, f"{label}: non-int {value!r} -> ceiling {ceiling}", 2)
114 return ceiling
115 if v > ceiling:
116 _dbg("OC_CHAT.CORE", func, f"{label}: clamped {v} -> {ceiling}", 2)
117 return ceiling
118 return v
119
120
121def clamp_timeout(timeout: object) -> int:
122 """Clamp a model ``timeout`` (seconds) to :data:`MAX_TIMEOUT_S`."""
123 return _clamp_upper(timeout, MAX_TIMEOUT_S, label="timeout")
124
125
126def clamp_bytes(n: object) -> int:
127 """Clamp a model byte/char budget to :data:`MAX_BYTES` (5 MB)."""
128 return _clamp_upper(n, MAX_BYTES, label="bytes")
129
130
131def clamp_results(n: object) -> int:
132 """Clamp a model result count to :data:`MAX_SEARCH_RESULTS`."""
133 return _clamp_upper(n, MAX_SEARCH_RESULTS, label="results")
134
135
136def clamp_depth(n: object) -> int:
137 """Clamp a model directory-walk depth to :data:`MAX_DIR_DEPTH`."""
138 return _clamp_upper(n, MAX_DIR_DEPTH, label="depth")
139
140
141def clamp_workers(n: int) -> int:
142 """Bound a parallel-worker pool to :data:`MAX_PARALLEL_WORKERS` (≥ 1)."""
143 try:
144 v = int(n)
145 except (TypeError, ValueError, OverflowError):
146 return 1
147 return max(1, min(MAX_PARALLEL_WORKERS, v))
148
149
151 """The effective per-turn tool-call ceiling — an ABSOLUTE ceiling a settings
152 write may LOWER but never RAISE (F33/§5.4).
153
154 The workspace ``max_tool_calls_per_turn`` setting (a model-reachable,
155 human-confirm-tier write) can only *lower* the effective cap; it can never
156 raise it above the operator's out-of-band ceiling (the shipped value
157 :data:`MAX_TOOL_CALLS_PER_TURN`, or the OOB env knob
158 :data:`_CAP_CEILING_ENV_VAR` when set, which ``_env_guard`` keeps off the
159 ``.env`` path). So the F52 "non-overridable by model args" DoS bound holds
160 against the settings-file path too. Degrades to the bounded default on a
161 non-int / unavailable settings read.
162
163 Plan 238 (CONFIG-01) moved the rule itself into
164 :func:`oc_chat.core.defaults.ceiling`, which applies it to every ceiling in
165 the system; this function keeps its name, its callers and its semantics.
166 """
167 return ceiling("tool_calls_per_turn")
168
169
170def _settings_request(setting: str) -> "float | None":
171 """The workspace settings' numeric REQUEST for *setting*, or ``None``.
172
173 ``None`` means "no request was made" — the settings subsystem is
174 unavailable, the key is absent, the value is unparseable, or it is
175 non-positive (``0`` is the documented "no request" spelling). Every such
176 answer degrades the caller to its bounded default, never to "unbounded".
177 """
178 func = "_settings_request"
179 if not setting:
180 return None
181 try:
182 from oc_chat.core.settings import runtime_settings
183 raw = getattr(runtime_settings(), setting, None)
184 except Exception as exc: # noqa: BLE001 — settings unavailable ⇒ bounded default
185 _dbg("OC_CHAT.CORE", func, f"VALIDATION_ERROR: {type(exc).__name__}: {exc}", 3)
186 return None
187 try:
188 value = float(raw) # type: ignore[arg-type]
189 except (TypeError, ValueError):
190 return None
191 return value if value > 0 else None
192
193
194def _env_raised_ceiling(key: str) -> int:
195 """The shipped ceiling for *key*, raised ONLY by the env variable it names.
196
197 This is :func:`oc_chat.core.defaults.ceiling`'s first half, reached through
198 the public accessors. It exists because the two ceilings compose the F33
199 rule from OPPOSITE starting points, and one function cannot be both:
200
201 - ``tool_calls_per_turn``'s settings key carries a shipped-valued default
202 (``DEFAULT_SETTINGS["max_tool_calls_per_turn"] == shipped(...)``), so
203 "unset" is indistinguishable from "requested exactly the shipped value".
204 Its resolver is therefore ``min(request, env_or_shipped)`` and the env
205 raises the ceiling the settings request is clamped by — unchanged here.
206 - ``session_cost_hard_cap_usd``'s settings key defaults to ``0`` = "no
207 request", so an operator who exports the env variable and writes nothing
208 else must get the raised cap. That is P4's "raisable only out of band".
209
210 Both are the same rule — settings may LOWER, never RAISE; the operator's
211 out-of-band environment raises — applied to a key whose settings default is
212 a value, and to one whose settings default is a sentinel.
213 """
214 func = "_env_raised_ceiling"
215 absolute = int(shipped(key))
216 name = env_var(key)
217 if not name:
218 return absolute
219 raw = os.environ.get(name)
220 if raw is None:
221 return absolute
222 try:
223 candidate = int(raw)
224 except (TypeError, ValueError):
225 return absolute
226 if candidate <= 0:
227 return absolute
228 _dbg("OC_CHAT.CORE", func, f"{key}: OOB ceiling {name}={candidate}", 2)
229 return candidate
230
231
233 """The effective session dollar hard cap — settings may LOWER, env raises.
234
235 CAPS-01 / P4. The shipped ``session_cost_hard_cap_usd`` (50) is the
236 adopters' protection; the workspace settings key the table names may only
237 lower it (``0`` = no request); the ceiling itself moves only when the
238 operator exports :data:`_COST_CEILING_ENV_VAR` out of band, which
239 ``_env_guard`` keeps off the workspace ``.env`` path. Whole dollars: the cap
240 is an int so it reuses the same env parsing every other ceiling uses, and a
241 fractional cap would suggest a precision the honesty gate cannot deliver.
242 """
243 absolute = _env_raised_ceiling("session_cost_hard_cap_usd")
244 request = _settings_request(setting_key("session_cost_hard_cap_usd"))
245 if request is None:
246 return absolute
247 return min(absolute, int(request))
248
249
250def resolve_cost_thresholds_usd() -> tuple[float, ...]:
251 """The effective session-cost checkpoint ladder, ascending.
252
253 CAPS-01 / P3, under the same F33 rule as every ceiling: the shipped ladder
254 is a FLOOR of protection. A workspace settings list may add checkpoints
255 BELOW the shipped ceiling — which only makes the session ask sooner — and
256 can neither remove a shipped checkpoint nor add one above the top of the
257 shipped ladder (that would move the last warning later, i.e. raise a bound
258 from a model-reachable file). An unavailable or malformed settings value
259 leaves the shipped ladder exactly as it is.
260 """
261 func = "resolve_cost_thresholds_usd"
262 ladder = [float(v) for v in shipped("session_cost_thresholds_usd")]
263 top = max(ladder)
264 setting = setting_key("session_cost_thresholds_usd")
265 extra: list[float] = []
266 if setting:
267 try:
268 from oc_chat.core.settings import runtime_settings
269 raw = getattr(runtime_settings(), setting, None)
270 except Exception as exc: # noqa: BLE001 — settings unavailable ⇒ shipped ladder
271 _dbg("OC_CHAT.CORE", func, f"VALIDATION_ERROR: {type(exc).__name__}: {exc}", 3)
272 raw = None
273 if isinstance(raw, (list, tuple)):
274 for item in raw:
275 try:
276 value = float(item)
277 except (TypeError, ValueError):
278 continue
279 if 0 < value < top:
280 extra.append(value)
281 else:
282 _dbg("OC_CHAT.CORE", func,
283 f"settings checkpoint {value!r} ignored: not below the "
284 f"shipped ceiling {top}", 3)
285 return tuple(sorted(set(ladder) | set(extra)))
286
287
288def partition_call_budget(prior: int, n: int, cap: "int | None" = None) -> tuple[int, int]:
289 """Return ``(allowed, denied)`` for *n* new tool calls this iteration given
290 *prior* calls already spent this turn, capped at *cap* (default: the resolved
291 per-turn ceiling, :func:`resolve_max_tool_calls_per_turn`)."""
292 if cap is None:
294 allowed = max(0, min(n, cap - max(0, prior)))
295 return allowed, n - allowed
296
297
298__all__ = [
299 "MAX_TOOL_CALLS_PER_TURN",
300 "MAX_PARALLEL_WORKERS",
301 "MAX_TIMEOUT_S",
302 "MAX_BYTES",
303 "MAX_SEARCH_RESULTS",
304 "MAX_DIR_DEPTH",
305 "clamp_timeout",
306 "clamp_bytes",
307 "clamp_results",
308 "clamp_depth",
309 "clamp_workers",
310 "partition_call_budget",
311 "resolve_cost_thresholds_usd",
312 "resolve_max_tool_calls_per_turn",
313 "resolve_session_cost_hard_cap_usd",
314]
tuple[int, int] partition_call_budget(int prior, int n, "int | None" cap=None)
int clamp_timeout(object timeout)
int _clamp_upper(object value, int ceiling, *, str label)
tuple[float,...] resolve_cost_thresholds_usd()
"float | None" _settings_request(str setting)