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/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)]
|