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/lock_modes.py ADDED
@@ -0,0 +1,185 @@
1
+ """Shared/exclusive lock modes — the sound way to recover read-concurrency.
2
+
3
+ Why this exists (the deterministic answer to the ⅓-ratio hazard)
4
+ ================================================================
5
+
6
+ `docs/114` §A1 found the `lane_overlap.OVERLAP_RATIO_MAX = 1/3` soft-overlap rule
7
+ **unsound**: lane conflict is treated as a *measure* ("how much of the requested
8
+ tree shares prefixes"), but fifty years of concurrency control (Gray, Lorie,
9
+ Putzolu, Traiger 1975/76, *Granularity of Locks*) make lock-compatibility a
10
+ **boolean predicate on the conflict** — two writers may share a contended region
11
+ only under operation *commutativity* (O'Neil 1986, escrow), which arbitrary file
12
+ overwrites lack. So any ratio > 0 admits genuine write–write conflicts on the
13
+ shared remainder — a silent lost-update `verify()` cannot catch.
14
+
15
+ §A1's *A-note* identified the actually-missing primitive, and §F deferred its
16
+ sound half here:
17
+
18
+ > The prefix-collision test (`_tree.prefixes_collide`) is **sound** as a
19
+ > conservative predicate-intersection check. What is genuinely missing is **lock
20
+ > MODES**: DOS has exactly one (taken / not-taken ≈ always-exclusive). So two
21
+ > read-only agents on `docs/**` conflict needlessly. A shared mode gives back
22
+ > read concurrency *soundly* — which is the concurrency the ⅓ hack reaches for
23
+ > *unsoundly*. **The absence of S-mode is what makes the ⅓ hack tempting.**
24
+
25
+ This module is that S-mode, as a **deterministic, pure, testable primitive** — not
26
+ prose in a plan. It is the classic two-mode lock-compatibility matrix combined with
27
+ the kernel's existing sound region-intersection predicate. No ratio, no model, no
28
+ I/O: `region_conflict` is a total boolean function of (tree, mode) × (tree, mode),
29
+ so it is replay-tested in isolation exactly like `lane_overlap.overlap_verdict`.
30
+
31
+ >>> from dos.lock_modes import LockMode, region_conflict
32
+ >>> # two readers on the same region — COMPATIBLE, no conflict:
33
+ >>> region_conflict(["docs/**"], LockMode.SHARED, ["docs/**"], LockMode.SHARED)
34
+ False
35
+ >>> # a writer vs a reader on the same region — INCOMPATIBLE:
36
+ >>> region_conflict(["docs/**"], LockMode.EXCLUSIVE, ["docs/**"], LockMode.SHARED)
37
+ True
38
+ >>> # two writers on DISJOINT regions — no intersection, so no conflict:
39
+ >>> region_conflict(["src/a/**"], LockMode.EXCLUSIVE, ["src/b/**"], LockMode.EXCLUSIVE)
40
+ False
41
+ >>> # two writers sharing ANY prefix — conflict at *any* overlap (no ratio):
42
+ >>> region_conflict(["src/api/x.py"], LockMode.EXCLUSIVE, ["src/api/**"], LockMode.EXCLUSIVE)
43
+ True
44
+
45
+ How it relates to the ⅓ rule
46
+ ============================
47
+
48
+ `region_conflict(..., EXCLUSIVE, ..., EXCLUSIVE)` is precisely the **sound
49
+ `ratio_max = 0` predicate**: two exclusive lanes conflict iff their regions
50
+ intersect *at all* (any shared prefix), with no fractional tolerance. So routing
51
+ write↔write through this module is the deterministic floor §A1 asked for; the
52
+ NEW capability it adds on top is that S/S no longer conflicts, recovering the
53
+ read-concurrency the ⅓ hack only reached for unsoundly. It is strictly a
54
+ **refine-and-tighten** of the existing predicate, never a loosening of the
55
+ write↔write case (which stays at zero-tolerance intersection).
56
+
57
+ Layering: pure stdlib + the `_tree` leaf it intersects with. A kernel leaf beside
58
+ `lane_overlap` / `overlap_policy`. No host names, no I/O. The arbiter/apply-gate
59
+ that *consumes* a per-lane mode (the PEP, `docs/119`) lives above this; this module
60
+ only decides the compatibility, the same way `overlap_verdict` decides overlap and
61
+ the caller acts on it.
62
+ """
63
+ from __future__ import annotations
64
+
65
+ from enum import Enum
66
+
67
+ from dos._tree import norm_tree_prefix as _norm_tree_prefix
68
+ from dos._tree import prefixes_collide as _prefixes_collide
69
+
70
+
71
+ class LockMode(str, Enum):
72
+ """The two lock modes a lane may hold over its region.
73
+
74
+ ``str``-valued so a mode round-trips through JSON / a WAL record / a
75
+ ``dos.toml`` field as its lowercase name without a custom codec — the same
76
+ convention every other kernel enum uses (``Verdict``, ``LivenessVerdict``).
77
+
78
+ * ``SHARED`` — a *read* lock: the lane reads the region but does not write it
79
+ (an audit, a `verify` fan-out, a render, a read-only analysis). Multiple
80
+ SHARED holders over the same region are mutually compatible.
81
+ * ``EXCLUSIVE`` — a *write* lock: the lane may mutate any path in the region.
82
+ Incompatible with every other holder (SHARED or EXCLUSIVE) over an
83
+ intersecting region. This is DOS's historical *only* mode — a lane with no
84
+ declared mode is EXCLUSIVE, so existing behavior is unchanged by default
85
+ (see ``DEFAULT_MODE``).
86
+ """
87
+ SHARED = "shared"
88
+ EXCLUSIVE = "exclusive"
89
+
90
+
91
+ #: The mode a lane holds when it declares none. EXCLUSIVE — the conservative
92
+ #: default that reproduces DOS's pre-S-mode behavior byte-for-byte (every lane
93
+ #: was effectively a write lock). Opting INTO ``SHARED`` is the only way to widen
94
+ #: concurrency, and it is the caller's explicit, auditable choice — never inferred.
95
+ DEFAULT_MODE: LockMode = LockMode.EXCLUSIVE
96
+
97
+
98
+ #: The lock-compatibility relation (Gray et al. 1975). The whole soundness of this
99
+ #: module is in this one table, so it is written as explicit data, not derived:
100
+ #: only SHARED↔SHARED is compatible; anything involving an EXCLUSIVE conflicts.
101
+ #: Symmetric by construction (every unordered pair is listed once each way), which
102
+ #: is the property the ⅓ ratio rule provably *lacked* (the 2026-06-01 TM↔tailor
103
+ #: asymmetric wedge — `docs/114` §A1).
104
+ _MODES_COMPATIBLE: dict[tuple[LockMode, LockMode], bool] = {
105
+ (LockMode.SHARED, LockMode.SHARED): True,
106
+ (LockMode.SHARED, LockMode.EXCLUSIVE): False,
107
+ (LockMode.EXCLUSIVE, LockMode.SHARED): False,
108
+ (LockMode.EXCLUSIVE, LockMode.EXCLUSIVE): False,
109
+ }
110
+
111
+
112
+ def modes_compatible(a: LockMode, b: LockMode) -> bool:
113
+ """True iff two lock modes may be held over an INTERSECTING region at once.
114
+
115
+ Pure lookup into the Gray-1975 compatibility matrix — the boolean
116
+ lock-compat relation, total over the two-mode lattice and symmetric. This is
117
+ *only* the mode half of the decision; whether the two regions intersect is
118
+ `_tree.prefixes_collide`, combined in `region_conflict`.
119
+ """
120
+ return _MODES_COMPATIBLE[(a, b)]
121
+
122
+
123
+ def _trees_intersect(req_tree: list[str], lease_tree: list[str]) -> bool:
124
+ """True iff any normalized prefix of one tree collides with one of the other.
125
+
126
+ The sound, zero-tolerance region-intersection test — `_tree.prefixes_collide`
127
+ (one prefix is a prefix of the other) applied pairwise. This is the
128
+ ``ratio_max = 0`` predicate: ANY shared prefix is an intersection, with no
129
+ fractional dilution. Literally-blank entries (falsy before normalization)
130
+ carry no path and are dropped; a leading-glob entry (``**/*`` → the universal
131
+ empty prefix) is KEPT and collides with everything, exactly as
132
+ `lane_overlap._shared_count` and `_tree.lane_trees_disjoint` treat it.
133
+ """
134
+ if not req_tree or not lease_tree:
135
+ # Unknown blast radius is the CALLER's asymmetry to enforce (cf.
136
+ # `_tree.lane_trees_disjoint`, `DisjointnessPredicate`); an empty tree is
137
+ # not "no region," so this low-level helper reports "no provable
138
+ # intersection" (False) and lets the caller apply the empty-tree refuse.
139
+ return False
140
+ req_prefixes = [_norm_tree_prefix(p) for p in req_tree if p]
141
+ lease_prefixes = [_norm_tree_prefix(p) for p in lease_tree if p]
142
+ if not req_prefixes or not lease_prefixes:
143
+ return False
144
+ for nr in req_prefixes:
145
+ for nl in lease_prefixes:
146
+ if _prefixes_collide(nr, nl):
147
+ return True
148
+ return False
149
+
150
+
151
+ def region_conflict(
152
+ requested_tree: list[str],
153
+ requested_mode: LockMode,
154
+ lease_tree: list[str],
155
+ lease_mode: LockMode,
156
+ ) -> bool:
157
+ """True iff a lane may NOT run alongside a live lease, under lock modes.
158
+
159
+ The sound floor §A1 asked for, as one deterministic function:
160
+
161
+ conflict ⟺ regions intersect AND modes are incompatible
162
+
163
+ * Disjoint regions never conflict (whatever the modes) — they cannot touch the
164
+ same file. This is `_trees_intersect`, the zero-tolerance (no-ratio)
165
+ intersection predicate.
166
+ * Intersecting regions conflict **iff** the modes are incompatible
167
+ (`modes_compatible`): two SHARED (read) holders coexist; anything with an
168
+ EXCLUSIVE (write) holder conflicts.
169
+
170
+ The write↔write case (``EXCLUSIVE`` vs ``EXCLUSIVE``) reduces to *intersect at
171
+ all* — the sound ``ratio_max = 0`` predicate, with none of the ⅓ rule's
172
+ fractional admit-window. The only concurrency this adds over zero-tolerance
173
+ exclusive-locking is the *sound* one: SHARED↔SHARED over a shared region. So
174
+ `region_conflict` can only ever refuse-MORE than the ⅓ rule on writes, and
175
+ admit-more only on provably-safe read/read — never a write–write collision.
176
+
177
+ Empty-tree handling: `_trees_intersect` returns False on an empty tree (no
178
+ provable intersection), so this returns False (no conflict) — the caller MUST
179
+ apply the unknown-blast-radius refuse upstream (as `DisjointnessPredicate`
180
+ already does), exactly as it must for `lane_overlap.overlap_verdict`. This
181
+ function decides the *known-vs-known under modes* case only.
182
+ """
183
+ if not _trees_intersect(list(requested_tree), list(lease_tree)):
184
+ return False
185
+ return not modes_compatible(requested_mode, lease_mode)
dos/log_source.py ADDED
@@ -0,0 +1,395 @@
1
+ """The log-source seam — pluggable log adapters, sorted by who authored the byte.
2
+
3
+ docs/117 — the log-specific sequel to docs/93 (the accountability spectrum for
4
+ non-git sources) and docs/95 (OS-level evidence). It answers the objection that
5
+ sinks a naive "native log adapters" feature: *an LLM already runs a program and
6
+ reads its log — what does routing logs through DOS add?* The answer is the whole
7
+ kernel thesis restated for one input — **a log is only evidence when the party that
8
+ authored the bytes is not the party being judged.** When an agent runs a program and
9
+ reads its own log, the agent is simultaneously the actor and the witness; the log it
10
+ surfaces is a self-report wearing evidence's clothes (the docs/103 distrust-the-
11
+ self-report law, the docs/84 §3.1 forgeable floor). A log climbs to *evidence* only
12
+ when the bytes are authored elsewhere: the kernel (not the agent) launched the
13
+ process and read the OS exit code, or the bytes are an infrastructure fossil the
14
+ agent cannot author (a cloud audit trail, a CI record, a privileged journald tree).
15
+
16
+ This module is the **pure seam** the log backends plug into — field-for-field the
17
+ shape `judges` (the JUDGE rung) and `overlap_policy` (the disjointness scorer)
18
+ already proved: a Protocol, frozen value types, an unshadowable built-in baseline, a
19
+ by-name resolver over an entry-point group, and a fail-safe runner. Every *backend*
20
+ with real I/O surface (read a file, shell `journalctl`, call the CloudWatch API)
21
+ lives in a `drivers/*` module — it imports the kernel; the kernel never imports it
22
+ (the `drivers/__init__` rule, pinned by `tests/test_log_source.py`). The kernel ships
23
+ only the abstraction + the honest zero, exactly as it ships the `Judge` protocol +
24
+ `AbstainJudge` while every ruling judge lives in a driver.
25
+
26
+ The one idea that makes this a *verification* seam and not a log-aggregation library
27
+ =================================================================================
28
+
29
+ Every `LogSource` declares its **`accountability`** — where it sits on the docs/93
30
+ spectrum — as DATA on the source, never inferred from content. That tag is what makes
31
+ the docs/117 §2 *inversion law* structural rather than a convention a host must
32
+ remember:
33
+
34
+ * `AGENT_AUTHORED` — the floor. The agent (or a process it controls) wrote these
35
+ bytes: a pasted terminal buffer, the agent's own stdout, a `screen`/`tmux`
36
+ scrollback, a local app log. Convenient to ingest (~zero effort) and worthless as
37
+ ground truth for exactly that reason — the agent chose every byte that reached
38
+ here. A consumer routes this to a **JUDGE** (advisory, fail-to-abstain — `judges`),
39
+ a *hint*, never a deterministic verdict.
40
+ * `OS_RECORDED` — the OS authored it: a kernel-launched process's exit code +
41
+ captured stream (the docs/117 §5 acceptance prize), a privileged journald/Event-Log
42
+ tree the agent can't write. The agent cannot forge an OS exit status or backdate a
43
+ root-gated log entry. A consumer may ground an **oracle** verdict on it.
44
+ * `THIRD_PARTY` — infrastructure the agent does not control authored it: a cloud
45
+ audit trail, a load-balancer access log, a CI build record. Hard to ingest (API +
46
+ auth + parse) and the highest-value source for exactly that reason — a deploy or a
47
+ served request leaves *only* this fossil. An **oracle** verdict.
48
+
49
+ The *flexibility* lives in which source you wire (the provenance / which-signal); the
50
+ *adjudication* — JUDGE-vs-oracle — is a fixed function of the declared tag. That is the
51
+ docs/76 line held exactly, and it means a buggy or over-eager host cannot accidentally
52
+ promote a pasted log into a verdict: an `AGENT_AUTHORED` source has no path to the
53
+ oracle classifier by construction.
54
+
55
+ The inversion law, in one sentence (docs/117 §2)
56
+ ================================================
57
+
58
+ A log's ingestion-ease is *inversely* proportional to its evidentiary value, because
59
+ both are governed by the same variable: proximity to the agent. The sources easiest
60
+ to ingest (paste, own stdout) are the floor; the sources hardest to ingest (cloud
61
+ trails) are the strongest. So this seam is organized by `accountability`, never by
62
+ convenience — the convenient sources still get backends, but they self-declare the
63
+ floor rung and route to a judge.
64
+
65
+ Purity & layering
66
+ =================
67
+
68
+ Pure stdlib — an enum, two frozen value types, a built-in source that is always
69
+ unreachable, and resolver/runner helpers. NO provider surface, no I/O inside a
70
+ verdict, names no host. It sits in the kernel layer beside `judges`/`overlap_policy`/
71
+ `render` (which likewise hold a pure protocol + resolver while the implementations
72
+ live outside). Entry-point discovery (the one bit of I/O) happens at the call boundary
73
+ in `active_log_sources`, exactly as `active_judges` / `active_predicates` do.
74
+ """
75
+
76
+ from __future__ import annotations
77
+
78
+ import enum
79
+ import sys
80
+ from dataclasses import dataclass, field
81
+ from typing import Protocol, runtime_checkable
82
+
83
+
84
+ class Accountability(str, enum.Enum):
85
+ """Where a log source sits on the docs/93 accountability spectrum.
86
+
87
+ Carried as DATA on each `LogSource` (a declared property, never inferred from the
88
+ bytes), so a consumer routes by the tag and the docs/117 §2 inversion law is
89
+ structural: an `AGENT_AUTHORED` source physically cannot reach an oracle verdict
90
+ path. `str`-valued so it round-trips through a CLI token / JSON without a lookup
91
+ table (the `Liveness` / `Stance` idiom).
92
+
93
+ Ordered floor → strongest. The dangerous direction is "treat an agent-authored log
94
+ as if the OS or a third party wrote it" — so the tag a source declares is the
95
+ *ceiling* on how much a consumer may trust it, never a floor a consumer may raise.
96
+ """
97
+
98
+ AGENT_AUTHORED = "AGENT_AUTHORED" # the agent/its process wrote it — JUDGE hint only
99
+ OS_RECORDED = "OS_RECORDED" # the OS authored it (exit code, privileged journald)
100
+ THIRD_PARTY = "THIRD_PARTY" # infra the agent can't write (cloud trail, CI, LB log)
101
+
102
+ def __str__(self) -> str: # pragma: no cover - trivial
103
+ return self.value
104
+
105
+ @property
106
+ def is_agent_authored(self) -> bool:
107
+ """True iff this is the forgeable floor — a JUDGE hint, never a verdict source.
108
+
109
+ The one predicate a consumer needs to honor the inversion law: route
110
+ `is_agent_authored` evidence to a judge (advisory), everything else may ground
111
+ an oracle. Named so the routing reads in plain words at the call site
112
+ (`if ev.accountability.is_agent_authored: feed_a_judge(...)`).
113
+ """
114
+ return self is Accountability.AGENT_AUTHORED
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class LogEvidence:
119
+ """Frozen, caller-gathered log facts — the `verdict.py` Evidence half, for logs.
120
+
121
+ The `CiEvidence` / `ProgressEvidence` analogue: facts gathered at the boundary
122
+ (inside a backend's `gather`) and handed to a consuming verdict, which is pure.
123
+
124
+ source_name — the backend that produced this (`"paste"`, `"cloudwatch"`),
125
+ for the operator-facing reason + the JSON consumer.
126
+ accountability — the source's spectrum rung (above). The load-bearing field:
127
+ a consumer routes JUDGE-vs-oracle off this, never off content.
128
+ lines — the pulled log lines, in source order. Empty on a degrade.
129
+ reachable — was the source actually reached and read? **Defaults to
130
+ False** — the fail-safe zero: an evidence object that nobody
131
+ successfully populated reads as "no signal," never as an empty-
132
+ but-trusted log. A consumer treats `reachable=False` as
133
+ NO_SIGNAL/abstain, the honest floor a non-git artifact oracle
134
+ (the move-B driver template) degrades to.
135
+ detail — a one-line human note (why unreachable, or what was read), for
136
+ the reason string / `dos doctor` — legible distrust.
137
+
138
+ Two constructors make the two outcomes unmistakable and keep the fail-safe default
139
+ from being fat-fingered: `reached(...)` for a successful read, `no_signal(...)` for
140
+ every degrade. There is deliberately no third way to set `reachable=True`.
141
+ """
142
+
143
+ source_name: str
144
+ accountability: Accountability
145
+ lines: tuple[str, ...] = field(default_factory=tuple)
146
+ reachable: bool = False
147
+ detail: str = ""
148
+
149
+ @classmethod
150
+ def reached(
151
+ cls,
152
+ source_name: str,
153
+ accountability: Accountability,
154
+ lines: tuple[str, ...],
155
+ *,
156
+ detail: str = "",
157
+ ) -> "LogEvidence":
158
+ """The source was reached and read. The ONLY constructor that sets
159
+ `reachable=True` — so a reachable log is always a deliberate, populated read,
160
+ never an accident of the default."""
161
+ return cls(
162
+ source_name=source_name,
163
+ accountability=accountability,
164
+ lines=tuple(lines),
165
+ reachable=True,
166
+ detail=detail,
167
+ )
168
+
169
+ @classmethod
170
+ def no_signal(
171
+ cls,
172
+ source_name: str,
173
+ accountability: Accountability,
174
+ *,
175
+ detail: str = "",
176
+ ) -> "LogEvidence":
177
+ """The source could not be reached/read — the honest floor (no source wired,
178
+ auth failed, timeout, empty). `reachable=False`, no lines. What every failure
179
+ in `gather_log` degrades to, and what a consuming verdict reads as
180
+ NO_SIGNAL/abstain — never a fabricated pass (the `run_judge`
181
+ fail-safe-never-fail-open discipline)."""
182
+ return cls(
183
+ source_name=source_name,
184
+ accountability=accountability,
185
+ lines=(),
186
+ reachable=False,
187
+ detail=detail,
188
+ )
189
+
190
+ def to_dict(self) -> dict:
191
+ return {
192
+ "source_name": self.source_name,
193
+ "accountability": self.accountability.value,
194
+ "lines": list(self.lines),
195
+ "reachable": self.reachable,
196
+ "detail": self.detail,
197
+ }
198
+
199
+
200
+ @runtime_checkable
201
+ class LogSource(Protocol):
202
+ """The contract a backend implements to add a log adapter.
203
+
204
+ `name` is the token a resolver selects and `dos doctor` would list.
205
+ `accountability` is the source's declared spectrum rung — a CLASS-LEVEL property,
206
+ fixed by the backend, not chosen per call (a `paste` source is `AGENT_AUTHORED`
207
+ always; it has no honest path to a higher rung). `gather` is handed a `subject`
208
+ (an opaque correlation handle — a run-id, a commit SHA, a unit name; the backend
209
+ decides what it means) and the active `config` (read-only), and returns a
210
+ `LogEvidence`.
211
+
212
+ A backend MAY do I/O *inside* `gather` (read a file, shell `journalctl`, call an
213
+ API) — unlike a predicate or renderer, which are pure. That is exactly why a real
214
+ backend lives in a driver, outside the kernel boundary: this seam is where I/O
215
+ surface is allowed, the same latitude the `Judge` protocol gives a ruling judge.
216
+ The discipline that keeps it honest is not purity but **fail-safe** (enforced by
217
+ `gather_log`, below, not by trusting the backend) plus the **fixed accountability
218
+ tag** (a backend cannot lie its way up the spectrum at call time).
219
+ """
220
+
221
+ name: str
222
+ accountability: Accountability
223
+
224
+ def gather(self, subject: str, config: object) -> LogEvidence:
225
+ ...
226
+
227
+
228
+ class NullLogSource:
229
+ """The built-in, always-available source: it reaches nothing.
230
+
231
+ The log analogue of the `text` renderer / `AbstainJudge` — a trusted, unshadowable
232
+ fallback (`resolve_log_source` resolves built-ins first). It is the honest zero of
233
+ the seam: a workspace with NO log adapter wired still has a resolvable source, and
234
+ it returns `no_signal` for every subject (the safe, conservative behavior — a
235
+ consumer sees "no log signal," never a fabricated read).
236
+
237
+ Tagged `AGENT_AUTHORED` — the floor — so that even the *absence* of a real source
238
+ can never be mistaken for a trustworthy rung: the most a missing adapter can claim
239
+ is the least-trusted tag, and it is unreachable on top of that.
240
+ """
241
+
242
+ name = "null"
243
+ accountability = Accountability.AGENT_AUTHORED
244
+
245
+ def gather(self, subject: str, config: object) -> LogEvidence:
246
+ return LogEvidence.no_signal(
247
+ self.name,
248
+ self.accountability,
249
+ detail=(
250
+ "no log adapter wired — the built-in null source reaches nothing, so "
251
+ "this subject has no log signal (configure a dos.log_sources backend)."
252
+ ),
253
+ )
254
+
255
+
256
+ def gather_log(source: LogSource, subject: str, config: object) -> LogEvidence:
257
+ """Run one source against one subject, enforcing **fail-safe, never fail-open**.
258
+
259
+ The wrapper EVERY consumer should call instead of `source.gather(...)` directly —
260
+ it is what makes "a backend can never manufacture a trusted log by failing" a
261
+ structural guarantee rather than a hope (the `run_judge` discipline, restated for
262
+ logs):
263
+
264
+ * a source that **raises** (file missing, API timeout, a bug) → an unreachable
265
+ `no_signal` naming the failure. Never propagates; never a reachable read.
266
+ * a source that returns **anything that is not a `LogEvidence`** (None, a dict, a
267
+ list of lines, a duck-typed look-alike) → `no_signal`. We never read a foreign
268
+ object's `.reachable`/`.lines`, so no fabricated read can sneak through a wrong
269
+ return type.
270
+
271
+ The degrade preserves the source's declared `accountability` so a consumer still
272
+ routes correctly even on failure (an unreachable `THIRD_PARTY` source is still not
273
+ a judge hint — it is an oracle source that had no signal this time). The tag is
274
+ read defensively (`getattr`, defaulting to the floor) so even a malformed source
275
+ object cannot escape to a higher rung via the failure path.
276
+
277
+ Note the direction matches `run_judge` (an evidence/adjudication gatherer), not
278
+ `admission.run_predicates` (a safety gate):
279
+ a log source is *evidence-gathering*, so its safe failure is "no signal" (let the
280
+ consuming verdict abstain / report NO_SIGNAL), never "deny" and never "pass."
281
+ """
282
+ name = getattr(source, "name", type(source).__name__)
283
+ acct = getattr(source, "accountability", Accountability.AGENT_AUTHORED)
284
+ if not isinstance(acct, Accountability):
285
+ acct = Accountability.AGENT_AUTHORED
286
+ try:
287
+ ev = source.gather(subject, config)
288
+ except Exception as e: # fail-safe: a source that raises produces no signal
289
+ return LogEvidence.no_signal(
290
+ str(name),
291
+ acct,
292
+ detail=(
293
+ f"log source {name!r} raised ({e!r}) — no signal (an evidence "
294
+ f"gatherer that cannot read produces NO_SIGNAL, never a fabricated log)."
295
+ ),
296
+ )
297
+ if not isinstance(ev, LogEvidence):
298
+ return LogEvidence.no_signal(
299
+ str(name),
300
+ acct,
301
+ detail=(
302
+ f"log source {name!r} returned a {type(ev).__name__}, not a "
303
+ f"LogEvidence — no signal (a source that does not return the evidence "
304
+ f"type cannot be trusted to have read anything)."
305
+ ),
306
+ )
307
+ return ev
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # Resolution — built-in first, then the `dos.log_sources` entry-point group.
312
+ # ---------------------------------------------------------------------------
313
+
314
+ # The entry-point group a workspace/researcher registers a log backend under.
315
+ LOG_SOURCE_ENTRY_POINT_GROUP = "dos.log_sources"
316
+
317
+ # The built-in sources, resolvable by name and UNSHADOWABLE by a plugin (a plugin
318
+ # registering `null` cannot displace this one — built-ins resolve first). Only the
319
+ # conservative `null` baseline ships in the kernel; every reading backend lives in a
320
+ # driver/plugin (the kernel has no I/O/provider surface).
321
+ _BUILT_IN_SOURCES: dict[str, type] = {
322
+ NullLogSource.name: NullLogSource,
323
+ }
324
+
325
+
326
+ def _discover_entry_point_sources(*, _stderr=None) -> list[tuple[str, LogSource]]:
327
+ """Find log backends registered under the `dos.log_sources` entry-point group.
328
+
329
+ A backend plugin registers ``name = "pkg.module:SourceClass"`` in its
330
+ ``[project.entry-points."dos.log_sources"]``. We load each, instantiate it if it
331
+ is a class, and return ``(entry_point_name, source)`` pairs sorted by name (stable,
332
+ so listing order is deterministic). A plugin that fails to load is skipped with a
333
+ one-line stderr note rather than crashing — the same posture
334
+ `judges._discover_entry_point_judges` / predicate / renderer discovery take (a
335
+ broken third-party plugin is the operator's to fix, not a kernel fault).
336
+ """
337
+ stderr = _stderr if _stderr is not None else sys.stderr
338
+ out: list[tuple[str, LogSource]] = []
339
+ try:
340
+ from importlib.metadata import entry_points
341
+ except Exception: # pragma: no cover - importlib.metadata always present py3.11+
342
+ return out
343
+ try:
344
+ eps = entry_points(group=LOG_SOURCE_ENTRY_POINT_GROUP)
345
+ except TypeError: # pragma: no cover - py<3.10 selectable-API fallback
346
+ eps = entry_points().get(LOG_SOURCE_ENTRY_POINT_GROUP, []) # type: ignore[attr-defined]
347
+ except Exception: # pragma: no cover - defensive: never let discovery crash a call
348
+ return out
349
+ for ep in sorted(eps, key=lambda e: e.name):
350
+ try:
351
+ obj = ep.load()
352
+ source = obj() if isinstance(obj, type) else obj
353
+ except Exception as e: # pragma: no cover - depends on third-party plugin
354
+ print(
355
+ f"warning: log source plugin {ep.name!r} failed to load ({e}); skipping",
356
+ file=stderr,
357
+ )
358
+ continue
359
+ out.append((ep.name, source))
360
+ return out
361
+
362
+
363
+ def resolve_log_source(name: str, *, _stderr=None) -> LogSource:
364
+ """Resolve a log source by name: built-ins first, then `dos.log_sources` plugins.
365
+
366
+ Built-ins (`null`) resolve FIRST and cannot be shadowed by a plugin of the same
367
+ name — the trusted-fallback guarantee, identical to `resolve_judge` /
368
+ `resolve_renderer`. An unknown name fails LOUD with the known list (it never
369
+ silently degrades to `null`, which would hide a typo'd source name): the caller
370
+ asked for a specific adapter and getting a different one silently is exactly the
371
+ unannounced substitution the kernel refuses.
372
+ """
373
+ if name in _BUILT_IN_SOURCES:
374
+ return _BUILT_IN_SOURCES[name]()
375
+ discovered = dict(_discover_entry_point_sources(_stderr=_stderr))
376
+ if name in discovered:
377
+ return discovered[name]
378
+ known = sorted(set(_BUILT_IN_SOURCES) | set(discovered))
379
+ raise ValueError(f"unknown log source {name!r}; known: {', '.join(known)}")
380
+
381
+
382
+ def active_log_sources(*, _stderr=None) -> list[tuple[str, LogSource]]:
383
+ """Every resolvable source as ``(name, source)`` — built-ins THEN discovered
384
+ plugins. Does ENTRY-POINT DISCOVERY (I/O), so it is a call-boundary helper, never
385
+ called inside a verdict (the `active_judges` discipline)."""
386
+ built = [(n, cls()) for n, cls in _BUILT_IN_SOURCES.items()]
387
+ discovered = _discover_entry_point_sources(_stderr=_stderr)
388
+ return built + discovered
389
+
390
+
391
+ def active_log_source_names(*, _stderr=None) -> list[str]:
392
+ """The names of every active source (built-in + discovered) — what `dos doctor`
393
+ would list so an operator can see which log adapters are wired (the log analogue
394
+ of "see the active judges / predicates")."""
395
+ return [name for name, _src in active_log_sources(_stderr=_stderr)]