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/help_summary.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""`dos helped` — the operator-facing "what did DOS catch for me?" projection.
|
|
2
|
+
|
|
3
|
+
DOS fires a firehose of enforcement decisions every session — a SELF_MODIFY
|
|
4
|
+
block, a lane COLLISION refused, a tool-stream WARN re-surfaced — and each one is
|
|
5
|
+
durably banked as an `OP_ENFORCE` record on the lane WAL (`lane_journal`,
|
|
6
|
+
docs/189 §C4). But until this module **nobody ever told the operator it was
|
|
7
|
+
happening**: the hook emitted a `deny`/`additionalContext` to the *agent*, and the
|
|
8
|
+
record went to a JSONL file no human reads. So the substrate could be quietly
|
|
9
|
+
saving a fleet from a dozen self-overwrites a day and the person running it would
|
|
10
|
+
never know — the observability "ran out" one rung short of the human (the docs/204
|
|
11
|
+
§4 wall, applied to DOS's own value).
|
|
12
|
+
|
|
13
|
+
This module closes that last rung. It is a **read-only projection** over the
|
|
14
|
+
enforcement stream the WAL already carries (the `observe`/`decisions`/`trace`
|
|
15
|
+
contract): it reads the OP_ENFORCE records, folds them into a "DOS helped with N
|
|
16
|
+
things" rollup (by intervention rung, by typed reason class, by tool), and the
|
|
17
|
+
hook path uses its cadence helper to surface a one-line nudge in the operator's
|
|
18
|
+
normal flow every Nth fire. It mints no belief, takes no lease, adjudicates
|
|
19
|
+
*nothing new* — the verdicts it counts were minted by the sensors; this only
|
|
20
|
+
folds and renders them. Delete it and you lose the reader, not the data.
|
|
21
|
+
|
|
22
|
+
Design rules (inherited from `observe`/`verdict_journal` — the projection scope):
|
|
23
|
+
|
|
24
|
+
* **Pure where it can be.** `summarize()` / `should_nudge()` / `nudge_line()`
|
|
25
|
+
take records / an index and return data — entries in, value out, no disk — so
|
|
26
|
+
the suite folds them without touching a file. Only the CLI verb's single
|
|
27
|
+
`read_all` at the boundary touches the journal.
|
|
28
|
+
* **Byte-clean by construction (docs/138).** Every field this counts —
|
|
29
|
+
`intervention`, `reason_class`, `tool`, `withheld`, `ts` — is **env-authored**:
|
|
30
|
+
the kernel wrote the OP_ENFORCE record downstream of an already-decided verdict.
|
|
31
|
+
No agent narration enters the count; a run cannot self-report its way to a
|
|
32
|
+
bigger "helped" number.
|
|
33
|
+
* **"Helped" is the rungs that changed behavior.** By default a help is a BLOCK
|
|
34
|
+
(a refused/withheld call) or a WARN (a surfaced correction) — the two rungs that
|
|
35
|
+
actually intervened. A passive OBSERVE log is recorded but is NOT a help, so the
|
|
36
|
+
number stays honest and is never inflated by silent logging.
|
|
37
|
+
|
|
38
|
+
There is deliberately no writer here — this module only reads what the sensors
|
|
39
|
+
already banked, so it can never journal a "help" the kernel did not enforce.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
|
|
46
|
+
# The intervention rungs that count as a "help" — the two that changed behavior.
|
|
47
|
+
# A BLOCK refused/withheld a call; a WARN surfaced a correction. A passive OBSERVE
|
|
48
|
+
# is recorded on the WAL but is NOT a help (counting it would inflate the number
|
|
49
|
+
# with silent logging that intervened in nothing). DEFER is the "ask a human" rung
|
|
50
|
+
# — also a real intervention, so it counts. Closed set, matched case-folded.
|
|
51
|
+
HELP_RUNGS: tuple[str, ...] = ("BLOCK", "WARN", "DEFER")
|
|
52
|
+
|
|
53
|
+
# Plain-English, one-line meaning of each refusal class an operator sees in the
|
|
54
|
+
# rollup — the answer to "what does `admission`/`SELF_MODIFY`/… actually mean?"
|
|
55
|
+
# Keyed by the actual tokens the kernel writes to a record's `reason_class`: the
|
|
56
|
+
# `BASE_REASONS` refusal tokens an ENFORCE record can carry (`reasons.py`) + the
|
|
57
|
+
# `CLASS_BUDGET_EXHAUSTED` named arbiter refuse (`arbiter.py`) + the env-authored
|
|
58
|
+
# handler-name fallbacks (`admission`/`provenance`, written when a record predates
|
|
59
|
+
# the typed-token lift). Keys are matched case-insensitively so an older `admission`
|
|
60
|
+
# record and a typed `SELF_MODIFY` token both resolve. An unknown key degrades to no
|
|
61
|
+
# gloss — we never invent an explanation, so a token added to the vocabulary later
|
|
62
|
+
# without a gloss here just shows bare, exactly as today. This is reference DATA, not
|
|
63
|
+
# a verdict: it explains an already-counted help, it never decides whether one IS a help.
|
|
64
|
+
REASON_GLOSSARY: dict[str, str] = {
|
|
65
|
+
"SELF_MODIFY": "an agent tried to edit the kernel's own running code "
|
|
66
|
+
"while a loop was adjudicating it",
|
|
67
|
+
"UNKNOWN_LANE": "an agent requested a lane this workspace doesn't declare",
|
|
68
|
+
"SCHEMA_UNREADABLE": "a durable record was tagged at a schema version this "
|
|
69
|
+
"kernel predates — refused rather than mis-parsed",
|
|
70
|
+
"CLASS_BUDGET_EXHAUSTED": "the concurrency budget for this class of work was "
|
|
71
|
+
"already full",
|
|
72
|
+
# The env-authored handler-name fallbacks (written when a record predates the
|
|
73
|
+
# typed-token lift) — explained as the rung that proposed the block.
|
|
74
|
+
"admission": "the lane-admission rung refused the call (usually a SELF_MODIFY "
|
|
75
|
+
"edit or a held-lane collision)",
|
|
76
|
+
"provenance": "the provenance rung refused the call (the claimed effect could "
|
|
77
|
+
"not be witnessed)",
|
|
78
|
+
"UNCLASSIFIED": "the kernel refused the call but recorded no typed reason "
|
|
79
|
+
"(an older record, predating the reason-class lift)",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def explain_reason(reason_class: str) -> str:
|
|
84
|
+
"""The one-line plain-English meaning of a refusal class, or "" if unknown.
|
|
85
|
+
|
|
86
|
+
Case-insensitive lookup into `REASON_GLOSSARY`. Returns "" for an unknown class
|
|
87
|
+
so a renderer shows the bare token rather than an invented explanation — we never
|
|
88
|
+
guess what a class means. Pure (a string in, a string out)."""
|
|
89
|
+
if not reason_class:
|
|
90
|
+
return ""
|
|
91
|
+
return REASON_GLOSSARY.get(reason_class, REASON_GLOSSARY.get(reason_class.upper(), ""))
|
|
92
|
+
|
|
93
|
+
# The in-flow nudge cadence: surface once on the FIRST help of a session (so the
|
|
94
|
+
# operator learns the substrate is working), then every 5th after. `1` and every
|
|
95
|
+
# multiple of `_NUDGE_EVERY` from there (1, 5, 10, 15, …).
|
|
96
|
+
_NUDGE_EVERY = 5
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# The fold — OP_ENFORCE records in, a typed rollup out. Pure (no disk).
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class Example:
|
|
106
|
+
"""One concrete help, for the `--explain` drill-down — env-authored, never narrated.
|
|
107
|
+
|
|
108
|
+
`target` is the path(s) the refusal was about (from the kernel-written `reason`);
|
|
109
|
+
`tool` is the tool call; `ts` is when; `reason` is the kernel's own one-line
|
|
110
|
+
explanation. Every field is bytes the sensor authored downstream of the verdict
|
|
111
|
+
(docs/138), so an example is a faithful record of what DOS caught, not a story.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
target: str = ""
|
|
115
|
+
tool: str = ""
|
|
116
|
+
ts: str = ""
|
|
117
|
+
reason: str = ""
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict:
|
|
120
|
+
return {"target": self.target, "tool": self.tool, "ts": self.ts,
|
|
121
|
+
"reason": self.reason}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class HelpSummary:
|
|
126
|
+
"""The "DOS helped with N things" rollup over a set of OP_ENFORCE records.
|
|
127
|
+
|
|
128
|
+
`total` is the help count (BLOCK + WARN + DEFER records); `by_rung` /
|
|
129
|
+
`by_reason` / `by_tool` are the breakdowns; `withheld` is how many of the helps
|
|
130
|
+
were calls actually refused (the strictest, most defensible subset). `enforced`
|
|
131
|
+
is the count of *all* enforcement records seen (helps + passive OBSERVE), so a
|
|
132
|
+
renderer can be honest that some firings were observe-only. `examples` maps a
|
|
133
|
+
reason class to a few concrete `Example`s (for the `--explain` drill-down); it is
|
|
134
|
+
populated only when `with_examples=True` so the cheap rollup path stays cheap.
|
|
135
|
+
`since` / `latest` echo the time window the count covers (the first/last `ts`).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
total: int = 0
|
|
139
|
+
enforced: int = 0
|
|
140
|
+
withheld: int = 0
|
|
141
|
+
by_rung: dict[str, int] = field(default_factory=dict)
|
|
142
|
+
by_reason: dict[str, int] = field(default_factory=dict)
|
|
143
|
+
by_tool: dict[str, int] = field(default_factory=dict)
|
|
144
|
+
examples: dict[str, tuple[Example, ...]] = field(default_factory=dict)
|
|
145
|
+
since: str = ""
|
|
146
|
+
latest: str = ""
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def blocked(self) -> int:
|
|
150
|
+
return self.by_rung.get("BLOCK", 0)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def warned(self) -> int:
|
|
154
|
+
return self.by_rung.get("WARN", 0)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def deferred(self) -> int:
|
|
158
|
+
return self.by_rung.get("DEFER", 0)
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> dict:
|
|
161
|
+
out = {
|
|
162
|
+
"total": self.total,
|
|
163
|
+
"enforced": self.enforced,
|
|
164
|
+
"withheld": self.withheld,
|
|
165
|
+
"blocked": self.blocked,
|
|
166
|
+
"warned": self.warned,
|
|
167
|
+
"deferred": self.deferred,
|
|
168
|
+
"by_rung": dict(self.by_rung),
|
|
169
|
+
"by_reason": dict(self.by_reason),
|
|
170
|
+
"by_tool": dict(self.by_tool),
|
|
171
|
+
"since": self.since,
|
|
172
|
+
"latest": self.latest,
|
|
173
|
+
}
|
|
174
|
+
if self.examples:
|
|
175
|
+
out["examples"] = {
|
|
176
|
+
cls: [e.to_dict() for e in exs]
|
|
177
|
+
for cls, exs in self.examples.items()
|
|
178
|
+
}
|
|
179
|
+
out["glossary"] = {
|
|
180
|
+
cls: explain_reason(cls) for cls in self.by_reason
|
|
181
|
+
if explain_reason(cls)
|
|
182
|
+
}
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _rung_of(rec: dict) -> str:
|
|
187
|
+
"""The intervention rung of an OP_ENFORCE record, upper-cased.
|
|
188
|
+
|
|
189
|
+
Reads the top-level `intervention` token `enforce_entry` lifts (the cheap
|
|
190
|
+
forensic field), degrading an absent/blank token to "" — never guessed.
|
|
191
|
+
"""
|
|
192
|
+
val = rec.get("intervention") or ""
|
|
193
|
+
return str(val).strip().upper()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def is_help(rec: dict, *, help_rungs: tuple[str, ...] = HELP_RUNGS) -> bool:
|
|
197
|
+
"""True iff this OP_ENFORCE record is a behavior-changing help (BLOCK/WARN/DEFER).
|
|
198
|
+
|
|
199
|
+
The single predicate the whole module turns on — keep the "what counts" rule in
|
|
200
|
+
exactly one place. A record whose `op` is not ENFORCE, or whose rung is OBSERVE
|
|
201
|
+
/ blank / unknown, is not a help.
|
|
202
|
+
"""
|
|
203
|
+
if rec.get("op") != "ENFORCE":
|
|
204
|
+
return False
|
|
205
|
+
return _rung_of(rec) in help_rungs
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# The env-authored target path(s) live in the parenthesized list inside the
|
|
209
|
+
# kernel-written `reason` text: `… running code (src/dos/arbiter.py, …) — refusing …`.
|
|
210
|
+
# We extract that list (and only that — a path-shaped, slash-or-backslash token), so
|
|
211
|
+
# the operator sees WHICH file was blocked. This reads the kernel's OWN sentence, not
|
|
212
|
+
# agent narration — the `reason` was authored by the sensor downstream of the verdict
|
|
213
|
+
# (docs/138), so the example stays byte-clean: a run cannot inject a path here.
|
|
214
|
+
_PAREN_PATHS = re.compile(r"\(([^)]*)\)")
|
|
215
|
+
_PATH_TOKEN = re.compile(r"[\w./\\-]+\.[\w]+")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _target_of(rec: dict) -> str:
|
|
219
|
+
"""The concrete path(s) a record's refusal was about, or "" — env-authored.
|
|
220
|
+
|
|
221
|
+
Pulls the parenthesized path list out of the kernel-written `reason` (e.g.
|
|
222
|
+
`(src/dos/arbiter.py)`), keeping only path-shaped tokens, joined back with ", ".
|
|
223
|
+
Falls back to the record's `proposal.reason` then the `lane`. Reads only
|
|
224
|
+
kernel-authored bytes; never the agent's. Pure (a record in, a string out)."""
|
|
225
|
+
text = str(rec.get("reason") or "")
|
|
226
|
+
if not text:
|
|
227
|
+
body = rec.get("proposal")
|
|
228
|
+
if isinstance(body, dict):
|
|
229
|
+
text = str(body.get("reason") or "")
|
|
230
|
+
for chunk in _PAREN_PATHS.findall(text):
|
|
231
|
+
paths = _PATH_TOKEN.findall(chunk)
|
|
232
|
+
if paths:
|
|
233
|
+
return ", ".join(paths)
|
|
234
|
+
return ""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _recover_reason_class(rec: dict) -> str:
|
|
238
|
+
"""The TYPED refusal class for a record, recovered as far as the env allows.
|
|
239
|
+
|
|
240
|
+
Prefers the top-level `reason_class`, then the SAME token nested in the
|
|
241
|
+
env-authored `proposal` body (present on older records whose top-level token was
|
|
242
|
+
never lifted — the 092ad29 gap), then the env-authored `handler` name, then
|
|
243
|
+
"UNCLASSIFIED". Every source is kernel-written; the human-readable `reason` prose
|
|
244
|
+
is never mined. This is the fix for the misleading "admission 597 / SELF_MODIFY 13"
|
|
245
|
+
split — both are SELF_MODIFY, but the older 597 lost their top-level token, so we
|
|
246
|
+
recover it from the proposal body and the two buckets collapse into the honest one."""
|
|
247
|
+
cls = str(rec.get("reason_class") or "").strip()
|
|
248
|
+
if cls:
|
|
249
|
+
return cls
|
|
250
|
+
body = rec.get("proposal")
|
|
251
|
+
if isinstance(body, dict):
|
|
252
|
+
cls = str(body.get("reason_class") or "").strip()
|
|
253
|
+
if cls:
|
|
254
|
+
return cls
|
|
255
|
+
return str(rec.get("handler") or "").strip() or "UNCLASSIFIED"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# How many distinct examples to bank per reason class for the `--explain` view.
|
|
259
|
+
# A small cap: the drill-down shows the SHAPE of what was caught, not the firehose.
|
|
260
|
+
_EXAMPLES_PER_REASON = 3
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def summarize(
|
|
264
|
+
records,
|
|
265
|
+
*,
|
|
266
|
+
holder: str = "",
|
|
267
|
+
since: str = "",
|
|
268
|
+
help_rungs: tuple[str, ...] = HELP_RUNGS,
|
|
269
|
+
with_examples: bool = False,
|
|
270
|
+
) -> HelpSummary:
|
|
271
|
+
"""Fold OP_ENFORCE records into a "DOS helped with N things" rollup. PURE.
|
|
272
|
+
|
|
273
|
+
`records` is any iterable of journal entries (the `lane_journal.read_all`
|
|
274
|
+
output, or a hand-built list in a test). `holder` filters to one session/owner
|
|
275
|
+
(the OP_ENFORCE `holder` is the session id — see `_journal_pretool_outcome`),
|
|
276
|
+
so the in-flow nudge and the stop digest can count *this* session's helps.
|
|
277
|
+
`since` keeps only records with `ts >= since` (ISO-8601 sorts lexically, so a
|
|
278
|
+
string compare is the window) — for `dos helped --since`. `help_rungs` is the
|
|
279
|
+
"what counts" set (defaults to BLOCK/WARN/DEFER). `with_examples` additionally
|
|
280
|
+
banks a few concrete `Example`s per reason class (for `dos helped --explain`) —
|
|
281
|
+
off by default so the cheap rollup path stays cheap.
|
|
282
|
+
|
|
283
|
+
Entries in, counts out, no disk — the unit-test surface, mirroring
|
|
284
|
+
`verdict_journal.rollup`.
|
|
285
|
+
"""
|
|
286
|
+
total = 0
|
|
287
|
+
enforced = 0
|
|
288
|
+
withheld = 0
|
|
289
|
+
by_rung: dict[str, int] = {}
|
|
290
|
+
by_reason: dict[str, int] = {}
|
|
291
|
+
by_tool: dict[str, int] = {}
|
|
292
|
+
examples: dict[str, list[Example]] = {}
|
|
293
|
+
seen_targets: dict[str, set[str]] = {}
|
|
294
|
+
first_ts = ""
|
|
295
|
+
last_ts = ""
|
|
296
|
+
|
|
297
|
+
for rec in records:
|
|
298
|
+
if rec.get("op") != "ENFORCE":
|
|
299
|
+
continue
|
|
300
|
+
if holder and str(rec.get("holder") or "") != holder:
|
|
301
|
+
continue
|
|
302
|
+
ts = str(rec.get("ts") or "")
|
|
303
|
+
if since and ts and ts < since:
|
|
304
|
+
continue
|
|
305
|
+
enforced += 1
|
|
306
|
+
if ts:
|
|
307
|
+
if not first_ts or ts < first_ts:
|
|
308
|
+
first_ts = ts
|
|
309
|
+
if ts > last_ts:
|
|
310
|
+
last_ts = ts
|
|
311
|
+
rung = _rung_of(rec)
|
|
312
|
+
if rung not in help_rungs:
|
|
313
|
+
continue # recorded (counted in `enforced`) but not a help
|
|
314
|
+
total += 1
|
|
315
|
+
by_rung[rung] = by_rung.get(rung, 0) + 1
|
|
316
|
+
if rec.get("withheld") is True:
|
|
317
|
+
withheld += 1
|
|
318
|
+
# The TYPED reason class, recovered as far as the env allows (top-level →
|
|
319
|
+
# the same token nested in the proposal body → the env-authored handler name
|
|
320
|
+
# → UNCLASSIFIED). All kernel-written — the `reason` prose is never mined.
|
|
321
|
+
# Recovering the nested token is what collapses the misleading
|
|
322
|
+
# "admission 597 / SELF_MODIFY 13" split into the honest single bucket.
|
|
323
|
+
reason_class = _recover_reason_class(rec)
|
|
324
|
+
by_reason[reason_class] = by_reason.get(reason_class, 0) + 1
|
|
325
|
+
tool = str(rec.get("tool") or "").strip() or "-"
|
|
326
|
+
by_tool[tool] = by_tool.get(tool, 0) + 1
|
|
327
|
+
|
|
328
|
+
if with_examples:
|
|
329
|
+
bank = examples.setdefault(reason_class, [])
|
|
330
|
+
if len(bank) < _EXAMPLES_PER_REASON:
|
|
331
|
+
target = _target_of(rec)
|
|
332
|
+
# Prefer DISTINCT targets so the few examples shown are different
|
|
333
|
+
# files, not the same path three times.
|
|
334
|
+
seen = seen_targets.setdefault(reason_class, set())
|
|
335
|
+
if not target or target not in seen:
|
|
336
|
+
if target:
|
|
337
|
+
seen.add(target)
|
|
338
|
+
reason_text = str(rec.get("reason") or "").strip()
|
|
339
|
+
body = rec.get("proposal")
|
|
340
|
+
if not reason_text and isinstance(body, dict):
|
|
341
|
+
reason_text = str(body.get("reason") or "").strip()
|
|
342
|
+
bank.append(Example(
|
|
343
|
+
target=target, tool=tool, ts=ts,
|
|
344
|
+
reason=_first_sentence(reason_text)))
|
|
345
|
+
|
|
346
|
+
return HelpSummary(
|
|
347
|
+
total=total,
|
|
348
|
+
enforced=enforced,
|
|
349
|
+
withheld=withheld,
|
|
350
|
+
by_rung=dict(sorted(by_rung.items(), key=lambda kv: (-kv[1], kv[0]))),
|
|
351
|
+
by_reason=dict(sorted(by_reason.items(), key=lambda kv: (-kv[1], kv[0]))),
|
|
352
|
+
by_tool=dict(sorted(by_tool.items(), key=lambda kv: (-kv[1], kv[0]))),
|
|
353
|
+
examples={cls: tuple(exs) for cls, exs in examples.items()},
|
|
354
|
+
since=first_ts,
|
|
355
|
+
latest=last_ts,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _first_sentence(text: str, *, limit: int = 160) -> str:
|
|
360
|
+
"""The first sentence of a kernel `reason`, trimmed for one-line display.
|
|
361
|
+
|
|
362
|
+
The full `reason` carries the explanation plus a "Pass --force …" trailer; the
|
|
363
|
+
operator-facing example wants just the first clause. Splits on the em-dash the
|
|
364
|
+
SELF_MODIFY/collision sentences use, else the first period, else hard-truncates.
|
|
365
|
+
Pure (a string in, a string out); reads kernel-authored bytes only."""
|
|
366
|
+
if not text:
|
|
367
|
+
return ""
|
|
368
|
+
for sep in ("—", " - ", ". "):
|
|
369
|
+
head = text.split(sep, 1)[0].strip()
|
|
370
|
+
if head and head != text:
|
|
371
|
+
return head[:limit].rstrip()
|
|
372
|
+
return text[:limit].rstrip()
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
# The cadence — when does the in-flow nudge fire? PURE (an index in, a bool out).
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def should_nudge(help_index: int, *, every: int = _NUDGE_EVERY) -> bool:
|
|
381
|
+
"""True iff the operator nudge should fire on this help (1-based count).
|
|
382
|
+
|
|
383
|
+
"First + every 5th": fire on the 1st help of the session (so the operator sees
|
|
384
|
+
the substrate is alive) and every `every`th after — indices 1, 5, 10, 15, ….
|
|
385
|
+
`help_index` is the running BLOCK/WARN/DEFER count *including* this firing
|
|
386
|
+
(1-based). A non-positive index never nudges.
|
|
387
|
+
"""
|
|
388
|
+
if help_index <= 0:
|
|
389
|
+
return False
|
|
390
|
+
if help_index == 1:
|
|
391
|
+
return True
|
|
392
|
+
return help_index % every == 0
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
# Rendering — the one-line in-flow nudge + the full operator rollup.
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def nudge_line(summary: HelpSummary) -> str:
|
|
401
|
+
"""The one-line in-flow nudge appended to the hook's additionalContext.
|
|
402
|
+
|
|
403
|
+
Operator-facing, single sentence, no narration: "DOS has caught N things this
|
|
404
|
+
session (X blocked, Y warned)." Surfaced on the 1st + every 5th help so the
|
|
405
|
+
operator learns, in their normal flow, that the substrate is working — without
|
|
406
|
+
a separate command and without nagging.
|
|
407
|
+
"""
|
|
408
|
+
parts: list[str] = []
|
|
409
|
+
if summary.blocked:
|
|
410
|
+
parts.append(f"{summary.blocked} blocked")
|
|
411
|
+
if summary.warned:
|
|
412
|
+
parts.append(f"{summary.warned} warned")
|
|
413
|
+
if summary.deferred:
|
|
414
|
+
parts.append(f"{summary.deferred} deferred")
|
|
415
|
+
detail = f" ({', '.join(parts)})" if parts else ""
|
|
416
|
+
noun = "thing" if summary.total == 1 else "things"
|
|
417
|
+
return (
|
|
418
|
+
f"DOS has caught {summary.total} {noun} this session{detail}. "
|
|
419
|
+
f"Run `dos helped` for the breakdown."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def render_summary_text(summary: HelpSummary, *, scope: str = "") -> str:
|
|
424
|
+
"""The full `dos helped` operator rollup — headline + breakdowns. Pure.
|
|
425
|
+
|
|
426
|
+
Leads with the headline count, then the by-reason-class and by-tool tables (the
|
|
427
|
+
"what kind of help, on which tool" an operator wants), and an honest footer
|
|
428
|
+
noting how many firings were observe-only (recorded but not a behavior-change).
|
|
429
|
+
"""
|
|
430
|
+
out: list[str] = []
|
|
431
|
+
title = "# dos helped"
|
|
432
|
+
if scope:
|
|
433
|
+
title += f" · {scope}"
|
|
434
|
+
out.append(title)
|
|
435
|
+
noun = "thing" if summary.total == 1 else "things"
|
|
436
|
+
out.append(f" DOS has caught {summary.total} {noun}"
|
|
437
|
+
+ (f" since {summary.since}" if summary.since else ""))
|
|
438
|
+
if summary.total:
|
|
439
|
+
rung_parts = []
|
|
440
|
+
if summary.blocked:
|
|
441
|
+
rung_parts.append(f"{summary.blocked} blocked")
|
|
442
|
+
if summary.warned:
|
|
443
|
+
rung_parts.append(f"{summary.warned} warned")
|
|
444
|
+
if summary.deferred:
|
|
445
|
+
rung_parts.append(f"{summary.deferred} deferred")
|
|
446
|
+
out.append(f" {', '.join(rung_parts)}"
|
|
447
|
+
+ (f" · {summary.withheld} calls actually refused"
|
|
448
|
+
if summary.withheld else ""))
|
|
449
|
+
if not summary.total:
|
|
450
|
+
out.append(" (no behavior-changing interventions recorded yet — "
|
|
451
|
+
"DOS has been observing, not blocking)")
|
|
452
|
+
if summary.enforced:
|
|
453
|
+
out.append(f" ({summary.enforced} enforcement record(s) seen, "
|
|
454
|
+
f"all observe-only)")
|
|
455
|
+
return "\n".join(out)
|
|
456
|
+
if summary.by_reason:
|
|
457
|
+
out.append("")
|
|
458
|
+
out.append(" by reason")
|
|
459
|
+
for reason, n in summary.by_reason.items():
|
|
460
|
+
gloss = explain_reason(reason)
|
|
461
|
+
line = f" {reason:<22} {n:>4}"
|
|
462
|
+
if gloss:
|
|
463
|
+
line += f" {gloss}" # the plain-English meaning, inline
|
|
464
|
+
out.append(line)
|
|
465
|
+
if summary.by_tool:
|
|
466
|
+
out.append("")
|
|
467
|
+
out.append(" by tool")
|
|
468
|
+
for tool, n in summary.by_tool.items():
|
|
469
|
+
out.append(f" {tool:<22} {n:>4}")
|
|
470
|
+
observe_only = summary.enforced - summary.total
|
|
471
|
+
if observe_only > 0:
|
|
472
|
+
out.append("")
|
|
473
|
+
out.append(f" ({observe_only} further firing(s) were observe-only — "
|
|
474
|
+
f"recorded, but changed nothing)")
|
|
475
|
+
# Point the operator at the drill-down — the answer to "but WHICH ones?"
|
|
476
|
+
out.append("")
|
|
477
|
+
out.append(" Run `dos helped --explain` for concrete examples (which files, why).")
|
|
478
|
+
return "\n".join(out)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def render_explain_text(summary: HelpSummary, *, scope: str = "") -> str:
|
|
482
|
+
"""The `dos helped --explain` drill-down — per reason class: meaning + examples.
|
|
483
|
+
|
|
484
|
+
The answer to "but WHICH ones, and what does `admission` mean?": for each reason
|
|
485
|
+
class, the plain-English gloss, the count, and a few concrete examples (the file
|
|
486
|
+
blocked, the tool, the kernel's own one-line reason). Every shown field is
|
|
487
|
+
env-authored (docs/138) — the gloss is reference data, the examples are bytes the
|
|
488
|
+
sensor wrote downstream of the verdict; no agent narration appears. Pure.
|
|
489
|
+
"""
|
|
490
|
+
out: list[str] = []
|
|
491
|
+
title = "# dos helped --explain"
|
|
492
|
+
if scope:
|
|
493
|
+
title += f" · {scope}"
|
|
494
|
+
out.append(title)
|
|
495
|
+
noun = "thing" if summary.total == 1 else "things"
|
|
496
|
+
out.append(f" DOS has caught {summary.total} {noun}"
|
|
497
|
+
+ (f" since {summary.since}" if summary.since else ""))
|
|
498
|
+
if not summary.total:
|
|
499
|
+
out.append("")
|
|
500
|
+
out.append(" (no behavior-changing interventions recorded yet — "
|
|
501
|
+
"DOS has been observing, not blocking)")
|
|
502
|
+
return "\n".join(out)
|
|
503
|
+
for reason, n in summary.by_reason.items():
|
|
504
|
+
out.append("")
|
|
505
|
+
plural = "block" if n == 1 else "blocks"
|
|
506
|
+
out.append(f" {reason} ({n} {plural})")
|
|
507
|
+
gloss = explain_reason(reason)
|
|
508
|
+
if gloss:
|
|
509
|
+
out.append(f" means: {gloss}")
|
|
510
|
+
exs = summary.examples.get(reason, ())
|
|
511
|
+
if exs:
|
|
512
|
+
out.append(" e.g.")
|
|
513
|
+
for e in exs:
|
|
514
|
+
where = e.target or "(target not recorded)"
|
|
515
|
+
tool = f" via {e.tool}" if e.tool and e.tool != "-" else ""
|
|
516
|
+
out.append(f" · {where}{tool}")
|
|
517
|
+
if e.reason:
|
|
518
|
+
out.append(f" {e.reason}")
|
|
519
|
+
return "\n".join(out)
|