dos-kernel 0.22.0__py3-none-win_amd64.whl

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 (178) hide show
  1. dos/__init__.py +261 -0
  2. dos/_bin/dos-hook.exe +0 -0
  3. dos/_filelock.py +255 -0
  4. dos/_job_policy.py +97 -0
  5. dos/_tree.py +145 -0
  6. dos/admission.py +433 -0
  7. dos/answer_shape.py +299 -0
  8. dos/arbiter.py +859 -0
  9. dos/archive_lock.py +266 -0
  10. dos/arg_provenance.py +814 -0
  11. dos/attest.py +472 -0
  12. dos/breaker.py +311 -0
  13. dos/churn.py +226 -0
  14. dos/claim_extract.py +229 -0
  15. dos/claim_ttl.py +150 -0
  16. dos/cli.py +8721 -0
  17. dos/commit_audit.py +666 -0
  18. dos/completion.py +466 -0
  19. dos/concurrency_class.py +154 -0
  20. dos/config.py +1380 -0
  21. dos/config_lint.py +464 -0
  22. dos/cooldown.py +390 -0
  23. dos/coverage.py +387 -0
  24. dos/dangling_intent.py +287 -0
  25. dos/data_class.py +397 -0
  26. dos/decisions.py +1274 -0
  27. dos/decisions_tui.py +251 -0
  28. dos/dispatch_top.py +740 -0
  29. dos/dispatch_top_tui.py +116 -0
  30. dos/drivers/__init__.py +40 -0
  31. dos/drivers/ci_status.py +630 -0
  32. dos/drivers/citation_resolve.py +703 -0
  33. dos/drivers/decision_stop.py +98 -0
  34. dos/drivers/export_file.py +173 -0
  35. dos/drivers/export_otlp.py +275 -0
  36. dos/drivers/export_statsd.py +242 -0
  37. dos/drivers/hook_dialects.py +391 -0
  38. dos/drivers/job.py +47 -0
  39. dos/drivers/llm_judge.py +360 -0
  40. dos/drivers/memory_recall.py +1231 -0
  41. dos/drivers/notify_slack.py +373 -0
  42. dos/drivers/notify_webhook.py +251 -0
  43. dos/drivers/operator_judge.py +114 -0
  44. dos/drivers/os_acceptance.py +228 -0
  45. dos/drivers/paste_log.py +132 -0
  46. dos/drivers/plan_scope.py +133 -0
  47. dos/drivers/self_improve.py +375 -0
  48. dos/drivers/similarity_judge.py +249 -0
  49. dos/drivers/state_diff.py +274 -0
  50. dos/drivers/supervisor.py +347 -0
  51. dos/drivers/watchdog.py +363 -0
  52. dos/drivers/workshop.py +160 -0
  53. dos/durable_schema.py +344 -0
  54. dos/effect_witness.py +393 -0
  55. dos/efficiency.py +318 -0
  56. dos/enforce.py +414 -0
  57. dos/enumerate.py +776 -0
  58. dos/env_print.py +378 -0
  59. dos/event_severity.py +258 -0
  60. dos/evidence.py +692 -0
  61. dos/exec_capability.py +256 -0
  62. dos/export_cursor.py +143 -0
  63. dos/exporter.py +320 -0
  64. dos/firing_label.py +353 -0
  65. dos/fleet_roll.py +226 -0
  66. dos/gate_classify.py +827 -0
  67. dos/gh4_coverage.py +179 -0
  68. dos/git_delta.py +122 -0
  69. dos/guard.py +215 -0
  70. dos/health.py +552 -0
  71. dos/help_summary.py +519 -0
  72. dos/home.py +934 -0
  73. dos/hook_binary.py +194 -0
  74. dos/hook_dialect.py +271 -0
  75. dos/hook_exit.py +191 -0
  76. dos/hook_install.py +437 -0
  77. dos/id_alloc.py +304 -0
  78. dos/improve.py +499 -0
  79. dos/intent_ledger.py +635 -0
  80. dos/interpret.py +176 -0
  81. dos/intervention.py +769 -0
  82. dos/intervention_eval.py +371 -0
  83. dos/journal_delta.py +308 -0
  84. dos/judge_eval.py +328 -0
  85. dos/judges.py +366 -0
  86. dos/lane_infer.py +127 -0
  87. dos/lane_journal.py +1001 -0
  88. dos/lane_lease.py +952 -0
  89. dos/lane_overlap.py +228 -0
  90. dos/lease_health.py +282 -0
  91. dos/lifecycle.py +211 -0
  92. dos/liveness.py +352 -0
  93. dos/lock_modes.py +185 -0
  94. dos/log_source.py +395 -0
  95. dos/loop_decide.py +1746 -0
  96. dos/marker_gate.py +254 -0
  97. dos/marker_sensor.py +396 -0
  98. dos/noop_streak.py +280 -0
  99. dos/notify.py +479 -0
  100. dos/observe.py +175 -0
  101. dos/oracle.py +1661 -0
  102. dos/overlap_eval.py +214 -0
  103. dos/overlap_policy.py +342 -0
  104. dos/packet_sidecar.py +267 -0
  105. dos/phase_shipped.py +1985 -0
  106. dos/pick_priority.py +225 -0
  107. dos/pickable.py +369 -0
  108. dos/picker_oracle.py +1037 -0
  109. dos/plan_board.py +513 -0
  110. dos/plan_board_tui.py +113 -0
  111. dos/plan_source.py +455 -0
  112. dos/posttool_sensor.py +528 -0
  113. dos/precursor_gate.py +499 -0
  114. dos/precursor_gate_eval.py +239 -0
  115. dos/preflight.py +825 -0
  116. dos/pretool_sensor.py +490 -0
  117. dos/proc_delta.py +181 -0
  118. dos/productivity.py +296 -0
  119. dos/provider_limit.py +242 -0
  120. dos/py.typed +4 -0
  121. dos/reason_morphology.py +299 -0
  122. dos/reasons.py +449 -0
  123. dos/reconcile.py +173 -0
  124. dos/recurring_wedge.py +206 -0
  125. dos/render.py +393 -0
  126. dos/result_state.py +468 -0
  127. dos/resume.py +578 -0
  128. dos/resume_evidence.py +293 -0
  129. dos/retention.py +344 -0
  130. dos/reward.py +372 -0
  131. dos/rewind.py +587 -0
  132. dos/rewind_evidence.py +168 -0
  133. dos/rewind_tokens.py +252 -0
  134. dos/run_id.py +342 -0
  135. dos/scope.py +520 -0
  136. dos/scope_source.py +382 -0
  137. dos/scout.py +982 -0
  138. dos/self_modify.py +209 -0
  139. dos/sibling_scan.py +569 -0
  140. dos/skills/EXAMPLES.md +584 -0
  141. dos/skills/dos-class-cycle/SKILL.md +107 -0
  142. dos/skills/dos-dispatch/SKILL.md +177 -0
  143. dos/skills/dos-dispatch-loop/SKILL.md +254 -0
  144. dos/skills/dos-goal-gate/SKILL.md +269 -0
  145. dos/skills/dos-next-up/SKILL.md +231 -0
  146. dos/skills/dos-promote/SKILL.md +114 -0
  147. dos/skills/dos-replan/SKILL.md +159 -0
  148. dos/skills/dos-replan-loop/SKILL.md +114 -0
  149. dos/skills/dos-self-improve/SKILL.md +213 -0
  150. dos/skills/dos-supervise-loop/SKILL.md +180 -0
  151. dos/skills/dos-unstick/SKILL.md +108 -0
  152. dos/skills/dos-witness-claim/SKILL.md +251 -0
  153. dos/stamp.py +1002 -0
  154. dos/state_health.py +387 -0
  155. dos/status.py +114 -0
  156. dos/stop_policy.py +334 -0
  157. dos/supervise.py +1014 -0
  158. dos/testwitness.py +392 -0
  159. dos/timeline.py +1027 -0
  160. dos/tokens.py +485 -0
  161. dos/tool_stream.py +393 -0
  162. dos/tool_stream_eval.py +226 -0
  163. dos/trace.py +524 -0
  164. dos/verdict.py +140 -0
  165. dos/verdict_cli.py +189 -0
  166. dos/verdict_journal.py +497 -0
  167. dos/verdict_rollup.py +217 -0
  168. dos/verdicts.py +181 -0
  169. dos/wedge_reason.py +282 -0
  170. dos_kernel-0.22.0.dist-info/METADATA +859 -0
  171. dos_kernel-0.22.0.dist-info/RECORD +178 -0
  172. dos_kernel-0.22.0.dist-info/WHEEL +5 -0
  173. dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
  174. dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
  175. dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
  176. dos_mcp/__init__.py +52 -0
  177. dos_mcp/py.typed +2 -0
  178. dos_mcp/server.py +779 -0
dos/enforce.py ADDED
@@ -0,0 +1,414 @@
1
+ """The enforcement-handler seam — *who consumes an intervention decision, and how.*
2
+
3
+ docs/189 §A1 (the Claude Code source audit). DOS is "a sound PDP with no PEP": the
4
+ kernel *decides* a verdict, it does not *enforce*. Two modules already split the
5
+ **decision** side of an in-flight catch:
6
+
7
+ * `dos.arg_provenance` / the detectors mint the VERDICT (was this id minted?).
8
+ * `dos.intervention` maps a verdict → an `InterventionDecision`: *at what strength*
9
+ should a consumer act (OBSERVE ‹ WARN ‹ BLOCK ‹ DEFER), confidence-gated, with a
10
+ synthetic-corrective payload builder for the turn-preserving BLOCK.
11
+
12
+ What was missing is the **consumer** side: the thing that TAKES an
13
+ `InterventionDecision` and *proposes the effect* — append a note, withhold the call
14
+ and substitute a synthetic result, re-prompt, or delegate the decision to a leader
15
+ agent. Today that dispatch is **hand-coded inline** in one consumer
16
+ (`benchmark.enterpriseops.dos_react`: an `if action is DEFER / BLOCK / OBSERVE`
17
+ ladder). The Claude Code permission system showed the clean shape — three policies
18
+ (`coordinatorHandler` / `interactiveHandler` / `swarmWorkerHandler`) behind **one**
19
+ seam, chosen by runtime context, not hardcoded. This module lifts that *shape*.
20
+
21
+ The seam, not the policies
22
+ ==========================
23
+
24
+ `EnforcementHandler` is the bring-your-own-PEP surface, the exact sibling of
25
+ `dos.judges` (the JUDGE rung) and `dos.overlap_policies` (the disjointness scorer):
26
+
27
+ * The KERNEL holds only the pure protocol + two frozen value types + a built-in
28
+ `ObserveHandler` (the unshadowable observe-only baseline) + `run_handler` (the
29
+ fail-safe wrapper) + a by-name resolver over the `dos.enforce_handlers`
30
+ entry-point group.
31
+ * Every *ruling* handler with real PEP surface — an interactive dialog, a swarm
32
+ delegate, an actual call-blocker, a sandbox wrapper — lives in a **driver** or a
33
+ plugin, discovered by name at the call boundary, never imported by the kernel
34
+ (the `drivers/__init__` one-way arrow). The handler returns a *proposal*; a host
35
+ PEP (`dos apply`, docs/126) is what finally acts. So DOS stays a PDP even with
36
+ this seam: the kernel proposes, the host enforces.
37
+
38
+ Fail-to-OBSERVE — the safe-failure direction for this role
39
+ ==========================================================
40
+
41
+ Each rung of the trust ladder has its own *safe* failure, and they point in
42
+ different directions on purpose:
43
+
44
+ * a safety **predicate** that cannot answer fails CLOSED → REFUSE
45
+ (`admission.run_predicates`): the safe direction for *admission* is "deny".
46
+ * an advisory **judge** that cannot answer fails to ABSTAIN (`judges.run_judge`):
47
+ the safe direction for *adjudication* is "ask a human".
48
+ * an enforcement **handler** that cannot answer fails to **OBSERVE**
49
+ (`run_handler`, here): the safe direction for *actuation* is "do nothing — let
50
+ the call through with a recorded note". A handler that RAISES, or returns a
51
+ non-`EffectProposal`, must NOT become a spurious BLOCK/DEFER — withholding a
52
+ legitimate call on a handler bug is how an advisory kernel turns into a
53
+ self-inflicted DoS (the docs/143 −9 pp lesson: disruption is the expensive
54
+ mistake). So a broken handler degrades to the zero-disruption proposal, never an
55
+ enforcement it never intended. `run_handler` makes that structural, exactly as
56
+ `run_judge` makes fail-to-abstain structural.
57
+
58
+ Note the asymmetry with `run_judge`: a judge's failure must never AGREE (auto-clear
59
+ a claim); a handler's failure must never *escalate* (auto-disrupt a call). Both
60
+ refuse to let a failure become the dangerous outcome for their role; they differ
61
+ only in which outcome is dangerous.
62
+
63
+ ⚓ Pure kernel, no I/O inside a proposal, advisory only — the dos idiom (mirrors
64
+ `dos.judges`, `dos.overlap_policies`). A handler MAY do I/O *inside* `handle` (an
65
+ interactive dialog reads a TTY, a delegate sends a message) — that is exactly why a
66
+ ruling handler lives outside the kernel boundary. The seam itself is pure: a
67
+ Protocol, two frozen value types, an observe-only built-in, and resolver/runner
68
+ helpers. Entry-point discovery (the one bit of I/O) happens at the call boundary in
69
+ `active_handlers`, never inside a proposal.
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ import sys
75
+ from dataclasses import dataclass
76
+ from typing import Protocol, runtime_checkable
77
+
78
+ from dos.intervention import Intervention, InterventionDecision
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # The proposal a handler returns — frozen, advisory (the kernel proposes, the
83
+ # host PEP acts). The actuation dual of `JudgeVerdict`.
84
+ # ---------------------------------------------------------------------------
85
+ @dataclass(frozen=True)
86
+ class EffectProposal:
87
+ """What a handler RECOMMENDS a host PEP do with an intervention — advisory, frozen.
88
+
89
+ A handler reads an `InterventionDecision` (the *strength*) and proposes the
90
+ concrete effect a host should materialize. It carries NOTHING it could mutate:
91
+ it is read by `dos apply` / a consumer loop / an operator, and acting on it is
92
+ always a separate, explicit step. This keeps the seam PDP-only — a handler can
93
+ no more "enforce itself into" a state change than a judge can believe itself
94
+ into one.
95
+
96
+ Fields:
97
+ intervention — the rung this proposal actuates (echoed from the decision,
98
+ possibly DE-escalated by a fail-safe; never escalated past
99
+ it — see `run_handler`). The closed `Intervention` ABI.
100
+ dispatch_call — should the host let the REAL tool call fire? True for
101
+ OBSERVE/WARN; False for BLOCK/DEFER. Read this, never infer
102
+ from the rung name (the `InterventionLadder.dispatches`
103
+ contract, carried onto the proposal).
104
+ synthetic_result — the corrective payload a host substitutes for a withheld
105
+ call (only set on a BLOCK; `dispatch_call` is then False).
106
+ Built by `intervention.synthetic_corrective_result`. None
107
+ when the call dispatches or the rung does not substitute.
108
+ note — advisory text a host may attach to the result / surface to
109
+ the agent (the WARN annotation, the OBSERVE ledger line).
110
+ handler — the name of the handler that produced this proposal (for
111
+ the audit ledger / `dos doctor`). Set by `run_handler`.
112
+ reason — one line: why this proposal, for the operator log.
113
+ """
114
+
115
+ intervention: Intervention
116
+ dispatch_call: bool
117
+ synthetic_result: dict | None = None
118
+ note: str = ""
119
+ handler: str = ""
120
+ reason: str = ""
121
+
122
+ def __post_init__(self) -> None:
123
+ # The one structural invariant: a synthetic result is only meaningful when
124
+ # the real call is withheld. A proposal that both dispatches the call AND
125
+ # substitutes a synthetic result is incoherent (the agent would see both the
126
+ # real effect and a "we blocked it" note) — reject it loudly, the
127
+ # `InterventionSpec.returns_synthetic implies not dispatches` discipline.
128
+ if self.synthetic_result is not None and self.dispatch_call:
129
+ raise ValueError(
130
+ "EffectProposal: a synthetic_result is substituted for a WITHHELD "
131
+ "call — dispatch_call must be False when synthetic_result is set"
132
+ )
133
+
134
+ @property
135
+ def withholds_call(self) -> bool:
136
+ """True iff the host should NOT fire the real call (`not dispatch_call`) —
137
+ the data-driven test a consumer reads, never a hardcoded `{BLOCK, DEFER}`."""
138
+ return not self.dispatch_call
139
+
140
+ def to_dict(self) -> dict:
141
+ return {
142
+ "intervention": self.intervention.value,
143
+ "dispatch_call": self.dispatch_call,
144
+ "synthetic_result": self.synthetic_result,
145
+ "note": self.note,
146
+ "handler": self.handler,
147
+ "reason": self.reason,
148
+ }
149
+
150
+
151
+ @runtime_checkable
152
+ class EnforcementHandler(Protocol):
153
+ """The contract a host implements to consume an intervention decision.
154
+
155
+ ``name`` is the token a consumer selects and `dos doctor` lists. ``handle`` is
156
+ handed the frozen `InterventionDecision` (the strength the kernel recommends) and
157
+ the active `config` (read-only) and returns an `EffectProposal` (the effect the
158
+ handler recommends a host PEP materialize).
159
+
160
+ A handler MAY do I/O *inside* ``handle`` (prompt a TTY, send a delegate message,
161
+ consult a sandbox) — unlike a predicate or renderer, which are pure. That is the
162
+ whole reason a real handler lives in a driver, outside the kernel boundary: this
163
+ is the PEP-adjacent rung where actuation surface is allowed. The disciplines that
164
+ keep it honest are advisory-only (it returns a proposal, mutates nothing) and
165
+ fail-to-observe (enforced by `run_handler`, not by trusting the handler), NOT
166
+ purity.
167
+ """
168
+
169
+ name: str
170
+
171
+ def handle(self, decision: InterventionDecision, config: object) -> EffectProposal:
172
+ ...
173
+
174
+
175
+ def _observe_proposal(decision: InterventionDecision, *, handler: str, reason: str) -> EffectProposal:
176
+ """The zero-disruption proposal: dispatch the real call, attach a recorded note.
177
+
178
+ The actuation floor — what every fail-safe and the built-in degrade to. Always
179
+ OBSERVE: the call fires unchanged, the verdict is recorded but the agent is not
180
+ interrupted. It can never withhold a call, so it is the one proposal a broken or
181
+ untrusted handler is allowed to fall back to.
182
+ """
183
+ return EffectProposal(
184
+ intervention=Intervention.OBSERVE,
185
+ dispatch_call=True,
186
+ synthetic_result=None,
187
+ note=decision.reason,
188
+ handler=handler,
189
+ reason=reason,
190
+ )
191
+
192
+
193
+ class ObserveHandler:
194
+ """The built-in, always-available handler: it proposes OBSERVE on everything.
195
+
196
+ The enforcement analogue of `judges.AbstainJudge` and the `text` renderer — a
197
+ trusted floor a plugin can never shadow (`resolve_handler` resolves built-ins
198
+ first). It is the honest zero of the seam: a workspace with NO PEP wired still
199
+ has a resolvable handler, and it records every verdict while letting every call
200
+ through (the safe, advisory, zero-disruption behavior — DOS's PDP-only default).
201
+ It is also the baseline a real handler is measured against: a handler that does
202
+ no better than OBSERVE has added enforcement nobody asked for.
203
+
204
+ Deliberately ignores the decision's *strength*: even a BLOCK/DEFER decision is
205
+ actuated as OBSERVE here. Escalating past OBSERVE is opt-in — it requires wiring
206
+ a ruling handler in a driver. The built-in never disrupts.
207
+ """
208
+
209
+ name = "observe"
210
+
211
+ def handle(self, decision: InterventionDecision, config: object) -> EffectProposal:
212
+ return _observe_proposal(
213
+ decision,
214
+ handler=self.name,
215
+ reason=(
216
+ "no enforcement handler wired — the built-in observes only: the call "
217
+ "dispatches unchanged and the verdict is recorded (configure a "
218
+ "dos.enforce_handlers driver to actuate BLOCK/DEFER)."
219
+ ),
220
+ )
221
+
222
+
223
+ def run_handler(
224
+ handler: EnforcementHandler, decision: InterventionDecision, config: object
225
+ ) -> EffectProposal:
226
+ """Run one handler against one decision, enforcing **fail-to-observe** AND the
227
+ **no-escalation** invariant. The wrapper EVERY consumer should call instead of
228
+ `handler.handle(...)` directly.
229
+
230
+ Two structural guarantees, both fail-SAFE toward less disruption:
231
+
232
+ 1. fail-to-observe — a handler that **raises** (a dialog times out, a delegate
233
+ is unreachable, a bug) → the zero-disruption OBSERVE proposal, naming the
234
+ failure. A handler that returns **anything that is not an `EffectProposal`**
235
+ (None, a dict, a duck-typed look-alike) → OBSERVE. We never read a foreign
236
+ object's `.withholds_call`, so no spurious block sneaks through a wrong
237
+ return type.
238
+ 2. no-escalation — a handler may DE-escalate (propose a rung no more disruptive
239
+ than the kernel recommended) but never ESCALATE past it. A handler that
240
+ returns a proposal MORE disruptive than the decision's rung is clamped back
241
+ down to the decision's rung as an OBSERVE-safe note. This makes "a handler
242
+ can never act harder than the kernel's confidence-gated recommendation" a
243
+ property of the wrapper, not a hope — the actuation analogue of judges'
244
+ "a failure can never auto-clear".
245
+
246
+ Both guarantees point the same way: a handler's failure or overreach degrades
247
+ toward LESS disruption, never more. Withholding a legitimate call on a handler
248
+ fault is the expensive mistake (the docs/143 −9 pp posture); this wrapper makes
249
+ it structurally unreachable by accident.
250
+ """
251
+ name = getattr(handler, "name", type(handler).__name__)
252
+ try:
253
+ proposal = handler.handle(decision, config)
254
+ except Exception as e: # fail-to-observe: a handler that raises cannot enforce
255
+ return _observe_proposal(
256
+ decision,
257
+ handler=name,
258
+ reason=(
259
+ f"handler {name!r} raised ({e!r}) — observing only (an actuation "
260
+ f"handler that faults must not withhold the call; it degrades to the "
261
+ f"zero-disruption proposal, never a spurious block)."
262
+ ),
263
+ )
264
+ if not isinstance(proposal, EffectProposal):
265
+ return _observe_proposal(
266
+ decision,
267
+ handler=name,
268
+ reason=(
269
+ f"handler {name!r} returned a {type(proposal).__name__}, not an "
270
+ f"EffectProposal — observing only (a handler that does not return the "
271
+ f"proposal type cannot be trusted to withhold a call)."
272
+ ),
273
+ )
274
+ # no-escalation: a handler may propose a LESS-or-equally disruptive rung, never a
275
+ # MORE disruptive one than the kernel's confidence-gated recommendation. Compare
276
+ # on the closed Intervention rank order (OBSERVE < WARN < BLOCK < DEFER). On
277
+ # overreach, degrade to the zero-disruption proposal rather than honor the
278
+ # escalation — the fail-safe direction.
279
+ if _rank(proposal.intervention) > _rank(decision.intervention):
280
+ return _observe_proposal(
281
+ decision,
282
+ handler=name,
283
+ reason=(
284
+ f"handler {name!r} proposed {proposal.intervention.value}, more "
285
+ f"disruptive than the kernel's {decision.intervention.value} "
286
+ f"recommendation — clamped to OBSERVE (a handler may de-escalate, "
287
+ f"never escalate past the confidence-gated rung)."
288
+ ),
289
+ )
290
+ # Stamp the handler name if the handler left it blank (so the audit ledger always
291
+ # records who produced the proposal) without mutating a frozen instance.
292
+ if not proposal.handler:
293
+ return EffectProposal(
294
+ intervention=proposal.intervention,
295
+ dispatch_call=proposal.dispatch_call,
296
+ synthetic_result=proposal.synthetic_result,
297
+ note=proposal.note,
298
+ handler=name,
299
+ reason=proposal.reason,
300
+ )
301
+ return proposal
302
+
303
+
304
+ # The fixed disruption order of the closed `Intervention` ABI — used only to enforce
305
+ # the no-escalation invariant in `run_handler`. This mirrors the canonical rank order
306
+ # in `BASE_INTERVENTIONS` (OBSERVE 0 < WARN 10 < BLOCK 20 < DEFER 30) but is kept as a
307
+ # tiny local total order on the ENUM so `run_handler` never needs a ladder instance to
308
+ # compare two rungs (a handler is selected per-decision; threading a ladder through
309
+ # would couple the seam to a ladder value it does not otherwise need). A host that adds
310
+ # a custom rung via `InterventionLadder.extend` still actuates through the closed enum,
311
+ # so this stays total over everything a handler can propose.
312
+ _INTERVENTION_RANK: dict[Intervention, int] = {
313
+ Intervention.OBSERVE: 0,
314
+ Intervention.WARN: 1,
315
+ Intervention.BLOCK: 2,
316
+ Intervention.DEFER: 3,
317
+ }
318
+
319
+
320
+ def _rank(intervention: Intervention) -> int:
321
+ """The disruption rank of an `Intervention` for the no-escalation check.
322
+
323
+ An unknown member (impossible for the closed enum, but defensive) ranks at the
324
+ TOP (max + 1) so it is treated as maximally disruptive — a value `run_handler`
325
+ cannot tell is safe is clamped, never let through. The conservative direction."""
326
+ return _INTERVENTION_RANK.get(intervention, max(_INTERVENTION_RANK.values()) + 1)
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # Resolution — built-in first, then the `dos.enforce_handlers` entry-point group.
331
+ # ---------------------------------------------------------------------------
332
+
333
+ # The entry-point group a workspace/researcher registers a handler under.
334
+ HANDLER_ENTRY_POINT_GROUP = "dos.enforce_handlers"
335
+
336
+ # The built-in handlers, resolvable by name and UNSHADOWABLE by a plugin (a plugin
337
+ # registering `observe` cannot displace this one — built-ins resolve first). Only the
338
+ # zero-disruption `observe` floor ships in the kernel; every ruling handler with PEP
339
+ # surface lives in a driver/plugin (the kernel has no actuation surface).
340
+ _BUILT_IN_HANDLERS: dict[str, type] = {
341
+ ObserveHandler.name: ObserveHandler,
342
+ }
343
+
344
+
345
+ def _discover_entry_point_handlers(*, _stderr=None) -> list[tuple[str, EnforcementHandler]]:
346
+ """Find handlers registered under the `dos.enforce_handlers` entry-point group.
347
+
348
+ A handler plugin registers ``name = "pkg.module:HandlerClass"`` in its
349
+ ``[project.entry-points."dos.enforce_handlers"]``. We load each, instantiate it if
350
+ it is a class, and return ``(entry_point_name, handler)`` pairs sorted by name
351
+ (stable, so `dos doctor` order is deterministic). A plugin that fails to load is
352
+ skipped with a one-line stderr note rather than crashing — the same posture
353
+ `judges._discover_entry_point_judges` / predicate / renderer discovery take (a
354
+ broken third-party plugin is the operator's to fix, not a kernel fault).
355
+ """
356
+ stderr = _stderr if _stderr is not None else sys.stderr
357
+ out: list[tuple[str, EnforcementHandler]] = []
358
+ try:
359
+ from importlib.metadata import entry_points
360
+ except Exception: # pragma: no cover - importlib.metadata always present py3.11+
361
+ return out
362
+ try:
363
+ eps = entry_points(group=HANDLER_ENTRY_POINT_GROUP)
364
+ except TypeError: # pragma: no cover - py<3.10 selectable-API fallback
365
+ eps = entry_points().get(HANDLER_ENTRY_POINT_GROUP, []) # type: ignore[attr-defined]
366
+ except Exception: # pragma: no cover - defensive: never let discovery crash a call
367
+ return out
368
+ for ep in sorted(eps, key=lambda e: e.name):
369
+ try:
370
+ obj = ep.load()
371
+ handler = obj() if isinstance(obj, type) else obj
372
+ except Exception as e: # pragma: no cover - depends on third-party plugin
373
+ print(
374
+ f"warning: enforce handler plugin {ep.name!r} failed to load ({e}); skipping",
375
+ file=stderr,
376
+ )
377
+ continue
378
+ out.append((ep.name, handler))
379
+ return out
380
+
381
+
382
+ def resolve_handler(name: str, *, _stderr=None) -> EnforcementHandler:
383
+ """Resolve a handler by name: built-ins first, then `dos.enforce_handlers` plugins.
384
+
385
+ Built-ins (`observe`) resolve FIRST and cannot be shadowed by a plugin of the same
386
+ name — the trusted-floor guarantee, identical to `resolve_judge` / `resolve_renderer`.
387
+ An unknown name fails LOUD with the known list (it never silently degrades to
388
+ `observe`, which would hide a typo'd handler selection): the caller asked for a
389
+ specific actuator and getting a different one silently is exactly the kind of
390
+ unannounced substitution the kernel refuses.
391
+ """
392
+ if name in _BUILT_IN_HANDLERS:
393
+ return _BUILT_IN_HANDLERS[name]()
394
+ discovered = dict(_discover_entry_point_handlers(_stderr=_stderr))
395
+ if name in discovered:
396
+ return discovered[name]
397
+ known = sorted(set(_BUILT_IN_HANDLERS) | set(discovered))
398
+ raise ValueError(f"unknown enforce handler {name!r}; known: {', '.join(known)}")
399
+
400
+
401
+ def active_handlers(*, _stderr=None) -> list[tuple[str, EnforcementHandler]]:
402
+ """Every resolvable handler as ``(name, handler)`` — built-ins THEN discovered
403
+ plugins, the order `dos doctor` lists. Does ENTRY-POINT DISCOVERY (I/O), so it is a
404
+ call-boundary helper, never called inside a proposal."""
405
+ built = [(n, cls()) for n, cls in _BUILT_IN_HANDLERS.items()]
406
+ discovered = _discover_entry_point_handlers(_stderr=_stderr)
407
+ return built + discovered
408
+
409
+
410
+ def active_handler_names(*, _stderr=None) -> list[str]:
411
+ """The names of every active handler (built-in + discovered) — what `dos doctor`
412
+ lists so an operator can see which actuators the enforcement seam can call (the
413
+ handler analogue of "see the active judges / predicates / reason set")."""
414
+ return [name for name, _handler in active_handlers(_stderr=_stderr)]