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.
- dos/__init__.py +261 -0
- dos/_bin/dos-hook.exe +0 -0
- dos/_filelock.py +255 -0
- dos/_job_policy.py +97 -0
- dos/_tree.py +145 -0
- dos/admission.py +433 -0
- dos/answer_shape.py +299 -0
- dos/arbiter.py +859 -0
- dos/archive_lock.py +266 -0
- dos/arg_provenance.py +814 -0
- dos/attest.py +472 -0
- dos/breaker.py +311 -0
- dos/churn.py +226 -0
- dos/claim_extract.py +229 -0
- dos/claim_ttl.py +150 -0
- dos/cli.py +8721 -0
- dos/commit_audit.py +666 -0
- dos/completion.py +466 -0
- dos/concurrency_class.py +154 -0
- dos/config.py +1380 -0
- dos/config_lint.py +464 -0
- dos/cooldown.py +390 -0
- dos/coverage.py +387 -0
- dos/dangling_intent.py +287 -0
- dos/data_class.py +397 -0
- dos/decisions.py +1274 -0
- dos/decisions_tui.py +251 -0
- dos/dispatch_top.py +740 -0
- dos/dispatch_top_tui.py +116 -0
- dos/drivers/__init__.py +40 -0
- dos/drivers/ci_status.py +630 -0
- dos/drivers/citation_resolve.py +703 -0
- dos/drivers/decision_stop.py +98 -0
- dos/drivers/export_file.py +173 -0
- dos/drivers/export_otlp.py +275 -0
- dos/drivers/export_statsd.py +242 -0
- dos/drivers/hook_dialects.py +391 -0
- dos/drivers/job.py +47 -0
- dos/drivers/llm_judge.py +360 -0
- dos/drivers/memory_recall.py +1231 -0
- dos/drivers/notify_slack.py +373 -0
- dos/drivers/notify_webhook.py +251 -0
- dos/drivers/operator_judge.py +114 -0
- dos/drivers/os_acceptance.py +228 -0
- dos/drivers/paste_log.py +132 -0
- dos/drivers/plan_scope.py +133 -0
- dos/drivers/self_improve.py +375 -0
- dos/drivers/similarity_judge.py +249 -0
- dos/drivers/state_diff.py +274 -0
- dos/drivers/supervisor.py +347 -0
- dos/drivers/watchdog.py +363 -0
- dos/drivers/workshop.py +160 -0
- dos/durable_schema.py +344 -0
- dos/effect_witness.py +393 -0
- dos/efficiency.py +318 -0
- dos/enforce.py +414 -0
- dos/enumerate.py +776 -0
- dos/env_print.py +378 -0
- dos/event_severity.py +258 -0
- dos/evidence.py +692 -0
- dos/exec_capability.py +256 -0
- dos/export_cursor.py +143 -0
- dos/exporter.py +320 -0
- dos/firing_label.py +353 -0
- dos/fleet_roll.py +226 -0
- dos/gate_classify.py +827 -0
- dos/gh4_coverage.py +179 -0
- dos/git_delta.py +122 -0
- dos/guard.py +215 -0
- dos/health.py +552 -0
- dos/help_summary.py +519 -0
- dos/home.py +934 -0
- dos/hook_binary.py +194 -0
- dos/hook_dialect.py +271 -0
- dos/hook_exit.py +191 -0
- dos/hook_install.py +437 -0
- dos/id_alloc.py +304 -0
- dos/improve.py +499 -0
- dos/intent_ledger.py +635 -0
- dos/interpret.py +176 -0
- dos/intervention.py +769 -0
- dos/intervention_eval.py +371 -0
- dos/journal_delta.py +308 -0
- dos/judge_eval.py +328 -0
- dos/judges.py +366 -0
- dos/lane_infer.py +127 -0
- dos/lane_journal.py +1001 -0
- dos/lane_lease.py +952 -0
- dos/lane_overlap.py +228 -0
- dos/lease_health.py +282 -0
- dos/lifecycle.py +211 -0
- dos/liveness.py +352 -0
- dos/lock_modes.py +185 -0
- dos/log_source.py +395 -0
- dos/loop_decide.py +1746 -0
- dos/marker_gate.py +254 -0
- dos/marker_sensor.py +396 -0
- dos/noop_streak.py +280 -0
- dos/notify.py +479 -0
- dos/observe.py +175 -0
- dos/oracle.py +1661 -0
- dos/overlap_eval.py +214 -0
- dos/overlap_policy.py +342 -0
- dos/packet_sidecar.py +267 -0
- dos/phase_shipped.py +1985 -0
- dos/pick_priority.py +225 -0
- dos/pickable.py +369 -0
- dos/picker_oracle.py +1037 -0
- dos/plan_board.py +513 -0
- dos/plan_board_tui.py +113 -0
- dos/plan_source.py +455 -0
- dos/posttool_sensor.py +528 -0
- dos/precursor_gate.py +499 -0
- dos/precursor_gate_eval.py +239 -0
- dos/preflight.py +825 -0
- dos/pretool_sensor.py +490 -0
- dos/proc_delta.py +181 -0
- dos/productivity.py +296 -0
- dos/provider_limit.py +242 -0
- dos/py.typed +4 -0
- dos/reason_morphology.py +299 -0
- dos/reasons.py +449 -0
- dos/reconcile.py +173 -0
- dos/recurring_wedge.py +206 -0
- dos/render.py +393 -0
- dos/result_state.py +468 -0
- dos/resume.py +578 -0
- dos/resume_evidence.py +293 -0
- dos/retention.py +344 -0
- dos/reward.py +372 -0
- dos/rewind.py +587 -0
- dos/rewind_evidence.py +168 -0
- dos/rewind_tokens.py +252 -0
- dos/run_id.py +342 -0
- dos/scope.py +520 -0
- dos/scope_source.py +382 -0
- dos/scout.py +982 -0
- dos/self_modify.py +209 -0
- dos/sibling_scan.py +569 -0
- dos/skills/EXAMPLES.md +584 -0
- dos/skills/dos-class-cycle/SKILL.md +107 -0
- dos/skills/dos-dispatch/SKILL.md +177 -0
- dos/skills/dos-dispatch-loop/SKILL.md +254 -0
- dos/skills/dos-goal-gate/SKILL.md +269 -0
- dos/skills/dos-next-up/SKILL.md +231 -0
- dos/skills/dos-promote/SKILL.md +114 -0
- dos/skills/dos-replan/SKILL.md +159 -0
- dos/skills/dos-replan-loop/SKILL.md +114 -0
- dos/skills/dos-self-improve/SKILL.md +213 -0
- dos/skills/dos-supervise-loop/SKILL.md +180 -0
- dos/skills/dos-unstick/SKILL.md +108 -0
- dos/skills/dos-witness-claim/SKILL.md +251 -0
- dos/stamp.py +1002 -0
- dos/state_health.py +387 -0
- dos/status.py +114 -0
- dos/stop_policy.py +334 -0
- dos/supervise.py +1014 -0
- dos/testwitness.py +392 -0
- dos/timeline.py +1027 -0
- dos/tokens.py +485 -0
- dos/tool_stream.py +393 -0
- dos/tool_stream_eval.py +226 -0
- dos/trace.py +524 -0
- dos/verdict.py +140 -0
- dos/verdict_cli.py +189 -0
- dos/verdict_journal.py +497 -0
- dos/verdict_rollup.py +217 -0
- dos/verdicts.py +181 -0
- dos/wedge_reason.py +282 -0
- dos_kernel-0.22.0.dist-info/METADATA +859 -0
- dos_kernel-0.22.0.dist-info/RECORD +178 -0
- dos_kernel-0.22.0.dist-info/WHEEL +5 -0
- dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
- dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
- dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
- dos_mcp/__init__.py +52 -0
- dos_mcp/py.typed +2 -0
- 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)]
|