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/precursor_gate.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""PG — the precursor-presence verdict: *did the mandated lookup FIRE before this mutation?*
|
|
2
|
+
|
|
3
|
+
docs/147 (the precursor-presence gate; greenlit by docs/146's slice survey). `arg_provenance`
|
|
4
|
+
(docs/143) asks *"did the model MINT this id, or RESOLVE it?"* — a check on
|
|
5
|
+
**provenance-of-a-string** that vanishes on a strong model (a model that reads-before-it-writes
|
|
6
|
+
mints nothing). docs/146 found one more byte-clean question of the same shape:
|
|
7
|
+
|
|
8
|
+
> does a tool whose name is on a config-declared mandated-precursor set produce ANY result in
|
|
9
|
+
> env-authored bytes before this mutating call's stream index?
|
|
10
|
+
|
|
11
|
+
That is **provenance-of-a-precursor-PRESENCE** — a pure byte question about *env-authored* bytes
|
|
12
|
+
(the gym MCP server authored the result, recording that the precursor tool fired; the judged
|
|
13
|
+
agent did not author the *existence* of that result). So it sidesteps the **mirror-verifier
|
|
14
|
+
trap** (docs/141, docs/143 §5a) for the same reason `arg_provenance` does: it needs no answer
|
|
15
|
+
key, no held-out state, and **no self-authored satisfaction predicate** ("did the precursor
|
|
16
|
+
result *authorize* this action on this resource?" — the forgeable-in-the-agent's-favor question
|
|
17
|
+
this module must never ask). It attacks the named "Missing Prerequisite Lookup" /
|
|
18
|
+
"Cascading State Propagation" failure modes that feed the Policy/Permission verifier slice.
|
|
19
|
+
|
|
20
|
+
The dead-line — firing, NEVER adjudication (docs/147 §3)
|
|
21
|
+
=======================================================
|
|
22
|
+
|
|
23
|
+
This module checks *only* that a mandated-precursor-named tool produced **some** result earlier
|
|
24
|
+
in the call stream. It deliberately does **not**, and structurally **cannot**, ask:
|
|
25
|
+
|
|
26
|
+
* **resource-identity** — "was the precursor about the SAME record the mutation touches?"
|
|
27
|
+
* **clause-satisfaction** — "did the precursor result return a value that AUTHORIZES the act?"
|
|
28
|
+
* **ordering beyond stream-index** — "does the precursor LOGICALLY precede this in the policy?"
|
|
29
|
+
|
|
30
|
+
Each of those binds the precursor *to this action* — a **provenance-of-a-RELATION** the agent
|
|
31
|
+
narrates from agent-visible prose, forgeable in its favor. The verdict's `REFUTED` therefore
|
|
32
|
+
means ONLY "a mandated precursor for this tool produced no result in the stream"
|
|
33
|
+
(presence-absence, a byte fact), never the OS-witnessed disconfirmation `evidence`'s `REFUTED`
|
|
34
|
+
carries for the `os_acceptance` driver. The moment a consumer lets this REFUTED drive an
|
|
35
|
+
actuating rung harder than WARN, it has crossed into the mirror-verifier — which is why the
|
|
36
|
+
intervention map below emits **only WARN** and has no harder rung to reach.
|
|
37
|
+
|
|
38
|
+
Why this is NOT `arg_provenance`'s `_build_env`/`_component_found` fold
|
|
39
|
+
======================================================================
|
|
40
|
+
|
|
41
|
+
Those fold over a prior result's *text tokens* — they answer "does this id-component trace to
|
|
42
|
+
env bytes?" A precursor tool's NAME is generally absent from its own result payload, so a
|
|
43
|
+
token-trace would systematically MISS a fired precursor (a false REFUTED). The firing question
|
|
44
|
+
is instead a **structural membership scan** over the call stream's `tool_name` fields:
|
|
45
|
+
`any(_canon(c.tool_name) in precursor_name_set for c in stream[:idx])`. This module lifts only
|
|
46
|
+
the *pattern* `arg_provenance`/`tool_stream` share — casefold + alias normalization + a pure
|
|
47
|
+
scan over an already-accumulated env corpus — not the id-matcher.
|
|
48
|
+
|
|
49
|
+
The two errors, and which one is safe
|
|
50
|
+
=====================================
|
|
51
|
+
|
|
52
|
+
* **false-NO_SIGNAL** (a mutating tool absent from the grammar, so never gated) → the call
|
|
53
|
+
dispatches as baseline. SAFE — no worse than not having the gate; the side-effect-suppression
|
|
54
|
+
edge is simply bounded by grammar coverage (what the eval's `missed_precursor_recall`
|
|
55
|
+
measures).
|
|
56
|
+
* **false-REFUTED** (the precursor DID fire, but under an alias the grammar did not list) → an
|
|
57
|
+
unnecessary WARN. Bounded by the `alias_map`, and even when it misses the cost is a redundant
|
|
58
|
+
reminder, not a withheld call (the intervention is WARN-only, §4) — a fire on a feasible task
|
|
59
|
+
is a *correct* "you have not called the mandated check" nudge, not a false-block.
|
|
60
|
+
|
|
61
|
+
So like `arg_provenance`, the whole module is tuned to **under-fire**, and its one actuation is
|
|
62
|
+
the least-disruptive informing rung.
|
|
63
|
+
|
|
64
|
+
⚓ Pure kernel, I/O on the edge (the dos idiom — mirrors `arg_provenance.classify_call`,
|
|
65
|
+
`liveness.classify`, `tool_stream.classify_stream`): `classify_call(MutatingCall, CallStream,
|
|
66
|
+
PrecursorGrammar, policy) -> PrecursorVerdict` is a frozen datum in, a frozen verdict out. The
|
|
67
|
+
caller flattens each prior tool RESULT to a `(tool_name, result_text)` pair at the boundary; the
|
|
68
|
+
kernel parses no JSON, reads no clock, no disk — replay-testable on frozen fixtures with zero
|
|
69
|
+
benchmark/LLM/MCP access.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
from __future__ import annotations
|
|
73
|
+
|
|
74
|
+
from dataclasses import dataclass, field
|
|
75
|
+
from pathlib import Path
|
|
76
|
+
from typing import Optional
|
|
77
|
+
|
|
78
|
+
# The verdict vocabulary has ONE home — docs/121's `evidence`. A precursor check is a
|
|
79
|
+
# presence-of-evidence question (did a witness — the precursor result — appear?), so it
|
|
80
|
+
# reuses `EvidenceStance` (ATTESTED / REFUTED / NO_SIGNAL) verbatim rather than fork a
|
|
81
|
+
# parallel three-valued enum. NB the SEMANTICS differ: here REFUTED is presence-absence,
|
|
82
|
+
# NOT the OS-witnessed disconfirmation `os_acceptance` mints it for (see the module doc).
|
|
83
|
+
from dos.evidence import EvidenceStance
|
|
84
|
+
|
|
85
|
+
# The actuation vocabulary — the shipped intervention ladder (docs/144). This gate maps its
|
|
86
|
+
# stance to a rung DIRECTLY (the `tool_stream` precedent), never via `choose_intervention`,
|
|
87
|
+
# which is typed to a `ProvenanceVerdict` this gate does not produce.
|
|
88
|
+
from dos.intervention import Intervention, InterventionDecision
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"PrecursorStance",
|
|
92
|
+
"PrecursorPolicy",
|
|
93
|
+
"DEFAULT_POLICY",
|
|
94
|
+
"PriorCall",
|
|
95
|
+
"CallStream",
|
|
96
|
+
"MutatingCall",
|
|
97
|
+
"PrecursorGrammar",
|
|
98
|
+
"PrecursorVerdict",
|
|
99
|
+
"classify_call",
|
|
100
|
+
"precursor_intervention",
|
|
101
|
+
"grammar_from_table",
|
|
102
|
+
"load_from_toml",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# The stance is `EvidenceStance` (one home for the presence-of-evidence vocabulary).
|
|
107
|
+
PrecursorStance = EvidenceStance
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _canon(name: str) -> str:
|
|
111
|
+
"""Normalize a tool name for matching: casefold + `-`/`.`→`_` (the `dos_react`
|
|
112
|
+
`_normalize_tool_name` convention, so `check-access` / `check.access` / `Check_Access`
|
|
113
|
+
all canonicalize to one key)."""
|
|
114
|
+
return (name or "").strip().casefold().replace("-", "_").replace(".", "_")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# Frozen inputs — the pure datum a caller gathers at the boundary and hands in.
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class PriorCall:
|
|
122
|
+
"""One prior call in the stream, as the pure datum the verdict sees.
|
|
123
|
+
|
|
124
|
+
`tool_name` is the tool the env executed (it returned a result, recording the firing).
|
|
125
|
+
`result_text` is the flattened result bytes — carried ONLY for the legibility note (a
|
|
126
|
+
consumer may quote it); the firing decision keys on `tool_name` alone (a structural
|
|
127
|
+
membership test, not a substring trace over `result_text` — see the module doc).
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
tool_name: str
|
|
131
|
+
result_text: str = ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class CallStream:
|
|
136
|
+
"""The env-authored call stream accumulated before the call under scrutiny.
|
|
137
|
+
|
|
138
|
+
`calls` is a tuple of `PriorCall` in call order — every prior tool RESULT the agent has
|
|
139
|
+
seen (the same `prior_tool_results` the `arg_provenance` consult already accumulates).
|
|
140
|
+
Empty (`()`) on the first call of an episode, which `classify_call` reads as "nothing
|
|
141
|
+
could have fired yet → NO_SIGNAL" (the load-bearing first-call safe direction, the
|
|
142
|
+
`arg_provenance` empty-corpus / `tool_stream` too-short floor).
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
calls: tuple[PriorCall, ...] = ()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class MutatingCall:
|
|
150
|
+
"""The call under scrutiny — the `ToolCall` / `AdmissionRequest` analogue.
|
|
151
|
+
|
|
152
|
+
`is_mutating` is set by the consumer's fail-open write-verb classifier (the same
|
|
153
|
+
`dos_react.is_mutating_tool`). A read / non-mutating call is never gated — reads are how a
|
|
154
|
+
precursor result ENTERS the stream — so `is_mutating=False` short-circuits to NO_SIGNAL.
|
|
155
|
+
The classifier is deliberately fail-open (when unsure, treat as a read): under-gating
|
|
156
|
+
degrades to baseline (safe), over-gating risks a feasible-task regression.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
tool_name: str
|
|
160
|
+
is_mutating: bool = True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class PrecursorPolicy:
|
|
165
|
+
"""The knobs — mechanism is kernel, knobs are config (the `LivenessPolicy` /
|
|
166
|
+
`ProvenancePolicy` / `StreamPolicy` seam). Defaults GENERIC; a host declares its own in
|
|
167
|
+
`dos.toml [precursor]` read back through `SubstrateConfig` (the closed-config-as-data
|
|
168
|
+
pattern, like `[tool_stream]` / `[intervention]`).
|
|
169
|
+
|
|
170
|
+
case_sensitive — match tool names case-sensitively. Default False (casefold both sides):
|
|
171
|
+
a precursor declared `Check_Access` still matches a called `check_access`
|
|
172
|
+
(the fewest-false-fires bias, the `arg_provenance` casefold default). NB
|
|
173
|
+
names are ALSO `-`/`.`→`_` normalized regardless (`_canon`), so this knob
|
|
174
|
+
only toggles the casefold step.
|
|
175
|
+
|
|
176
|
+
There is deliberately NO knob for resource-binding, clause-satisfaction, or ordering beyond
|
|
177
|
+
stream-index — those are the off-limits provenance-of-a-RELATION questions (the module doc's
|
|
178
|
+
dead-line, made structural by the absence of the field).
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
case_sensitive: bool = False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
DEFAULT_POLICY = PrecursorPolicy()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# The grammar — config-as-data: which mutating tool requires which precursor(s).
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class PrecursorGrammar:
|
|
192
|
+
"""The mandated-precursor map, as data — the unit a workspace declares.
|
|
193
|
+
|
|
194
|
+
`requires` maps a mutating tool name → the tuple of precursor tool name(s) that satisfy
|
|
195
|
+
its mandate (ANY one present in the stream → ATTESTED — the floor is "at least one mandated
|
|
196
|
+
lookup fired," never "all of them"). `aliases` maps a precursor name → other tool names
|
|
197
|
+
that count as the SAME precursor (the synonym allow-list — the §3 false-REFUTED safety
|
|
198
|
+
valve). Both are **Appendix-C / system-prompt-derived, NEVER inferred** — a host writes the
|
|
199
|
+
map by reading the policy prose once, the way it writes its lane taxonomy from the dir tree.
|
|
200
|
+
Inferring the map from policy text *is* parsing policy = planner-adjacent = off-limits.
|
|
201
|
+
|
|
202
|
+
The map is keyed on canonical names (`_canon`) at construction, so a lookup is a single
|
|
203
|
+
canonical-name membership test with no per-call normalization of the grammar.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
requires: dict = field(default_factory=dict)
|
|
207
|
+
aliases: dict = field(default_factory=dict)
|
|
208
|
+
|
|
209
|
+
def required_set(self, mutating_tool: str) -> frozenset:
|
|
210
|
+
"""The canonical set of tool names whose presence satisfies `mutating_tool`'s mandate —
|
|
211
|
+
the declared precursor(s) UNION every alias of each. Empty frozenset iff the tool has no
|
|
212
|
+
declared precursor (→ the caller NO_SIGNALs it). PURE."""
|
|
213
|
+
key = _canon(mutating_tool)
|
|
214
|
+
declared = self.requires.get(key)
|
|
215
|
+
if not declared:
|
|
216
|
+
return frozenset()
|
|
217
|
+
out: set[str] = set()
|
|
218
|
+
for p in declared:
|
|
219
|
+
cp = _canon(p)
|
|
220
|
+
out.add(cp)
|
|
221
|
+
for alias in self.aliases.get(cp, ()): # aliases keyed canonical (built below)
|
|
222
|
+
out.add(_canon(alias))
|
|
223
|
+
return frozenset(out)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
EMPTY_GRAMMAR = PrecursorGrammar()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# Frozen verdict — the folded answer, advisory only (the EvidenceFacts shape).
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
@dataclass(frozen=True)
|
|
233
|
+
class PrecursorVerdict:
|
|
234
|
+
"""The folded answer over a mutating call — the `ProvenanceVerdict` / `StreamVerdict` analogue.
|
|
235
|
+
|
|
236
|
+
`stance` is the typed `PrecursorStance` (= `EvidenceStance`):
|
|
237
|
+
ATTESTED — a mandated precursor for this tool produced a result earlier in the stream.
|
|
238
|
+
REFUTED — the call is mutating, HAS a declared mandated precursor, and NONE fired. The
|
|
239
|
+
one actionable rung (→ a WARN re-surfacing the requirement).
|
|
240
|
+
NO_SIGNAL — a read/non-mutating call, OR a mutating tool with no declared precursor, OR an
|
|
241
|
+
empty stream (first call). The fail-safe zero; never an intervention.
|
|
242
|
+
|
|
243
|
+
`mutating_tool` echoes the call. `required` is the canonical precursor set that would have
|
|
244
|
+
satisfied the mandate (the requirement the WARN names — never a fabricated DB row).
|
|
245
|
+
`present` is the subset of `required` actually found in the stream (empty ⟺ REFUTED among
|
|
246
|
+
mutating-with-grammar calls). `reason` is the one-line operator summary. Advisory: never
|
|
247
|
+
raises, never dispatches — the consumer reads `stance` and decides whether to re-surface.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
stance: PrecursorStance
|
|
251
|
+
mutating_tool: str
|
|
252
|
+
required: tuple[str, ...]
|
|
253
|
+
present: tuple[str, ...]
|
|
254
|
+
reason: str
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def fired(self) -> bool:
|
|
258
|
+
"""True iff this is the actionable REFUTED rung (the only stance that drives a WARN)."""
|
|
259
|
+
return self.stance is EvidenceStance.REFUTED
|
|
260
|
+
|
|
261
|
+
def to_dict(self) -> dict:
|
|
262
|
+
return {
|
|
263
|
+
"stance": self.stance.value,
|
|
264
|
+
"mutating_tool": self.mutating_tool,
|
|
265
|
+
"required": list(self.required),
|
|
266
|
+
"present": list(self.present),
|
|
267
|
+
"reason": self.reason,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# The pure verdict — a structural tool_name-membership scan over the stream.
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
def classify_call(
|
|
275
|
+
call: MutatingCall,
|
|
276
|
+
stream: CallStream,
|
|
277
|
+
grammar: PrecursorGrammar = EMPTY_GRAMMAR,
|
|
278
|
+
policy: PrecursorPolicy = DEFAULT_POLICY,
|
|
279
|
+
) -> PrecursorVerdict:
|
|
280
|
+
"""Classify whether `call`'s mandated precursor(s) fired earlier in `stream`. PURE — no I/O.
|
|
281
|
+
|
|
282
|
+
Reads the ladder top to bottom:
|
|
283
|
+
|
|
284
|
+
1. NO_SIGNAL — a read / non-mutating call (reads are how a precursor ENTERS, never gated).
|
|
285
|
+
2. NO_SIGNAL — a mutating call whose tool has NO declared precursor in the grammar (the
|
|
286
|
+
absent-key safe direction — the gate only speaks where a host declared a mandate; an
|
|
287
|
+
undeclared mutating tool dispatches as baseline). This is the grammar-coverage bound.
|
|
288
|
+
3. NO_SIGNAL — an empty stream (the first call): nothing could have fired yet, so we never
|
|
289
|
+
accuse (the `arg_provenance` empty-corpus floor).
|
|
290
|
+
4. ATTESTED — ≥1 of the mandated precursor names appears among the prior calls' tool names.
|
|
291
|
+
5. REFUTED — none did: the agent is about to mutate before the mandated lookup fired.
|
|
292
|
+
|
|
293
|
+
The match is a structural `tool_name`-membership test (canonicalized; casefolded unless
|
|
294
|
+
`policy.case_sensitive`), NOT a substring trace over result bytes — a precursor's name is
|
|
295
|
+
generally absent from its own result payload, so a token-trace would false-REFUTED.
|
|
296
|
+
"""
|
|
297
|
+
required = grammar.required_set(call.tool_name)
|
|
298
|
+
|
|
299
|
+
if not call.is_mutating:
|
|
300
|
+
return PrecursorVerdict(
|
|
301
|
+
stance=EvidenceStance.NO_SIGNAL,
|
|
302
|
+
mutating_tool=call.tool_name,
|
|
303
|
+
required=tuple(sorted(required)),
|
|
304
|
+
present=(),
|
|
305
|
+
reason="read / non-mutating call — precursor not gated (reads source the stream)",
|
|
306
|
+
)
|
|
307
|
+
if not required:
|
|
308
|
+
return PrecursorVerdict(
|
|
309
|
+
stance=EvidenceStance.NO_SIGNAL,
|
|
310
|
+
mutating_tool=call.tool_name,
|
|
311
|
+
required=(),
|
|
312
|
+
present=(),
|
|
313
|
+
reason=(
|
|
314
|
+
f"{call.tool_name!r} has no declared mandated precursor in the grammar — "
|
|
315
|
+
f"not gated (the side-effect-suppression edge is bounded by grammar coverage)"
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
if not stream.calls:
|
|
319
|
+
return PrecursorVerdict(
|
|
320
|
+
stance=EvidenceStance.NO_SIGNAL,
|
|
321
|
+
mutating_tool=call.tool_name,
|
|
322
|
+
required=tuple(sorted(required)),
|
|
323
|
+
present=(),
|
|
324
|
+
reason="empty call stream — first call of the episode, nothing could have fired yet",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# The structural membership scan: which mandated precursor names produced a prior result?
|
|
328
|
+
# `required` is canonical (delimiter-normalized + casefolded, via the grammar). When
|
|
329
|
+
# `case_sensitive` is OFF (the default), a prior call's name is matched the same way, so the
|
|
330
|
+
# comparison is like-for-like. When ON, the host has opted into exact-case matching: we
|
|
331
|
+
# compare delimiter-normalized-but-NOT-casefolded names on BOTH sides (re-deriving the
|
|
332
|
+
# required set without casefold) so a `Check_Access` precursor no longer matches a called
|
|
333
|
+
# `check_access`. The common path is the casefold default.
|
|
334
|
+
if policy.case_sensitive:
|
|
335
|
+
def _name(n: str) -> str:
|
|
336
|
+
return (n or "").strip().replace("-", "_").replace(".", "_")
|
|
337
|
+
req_match = frozenset(
|
|
338
|
+
_name(p)
|
|
339
|
+
for p in grammar.requires.get(_canon(call.tool_name), ())
|
|
340
|
+
) | frozenset(
|
|
341
|
+
_name(a)
|
|
342
|
+
for p in grammar.requires.get(_canon(call.tool_name), ())
|
|
343
|
+
for a in grammar.aliases.get(p, ())
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
_name = _canon
|
|
347
|
+
req_match = required
|
|
348
|
+
present = sorted({
|
|
349
|
+
_name(c.tool_name) for c in stream.calls if _name(c.tool_name) in req_match
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
if present:
|
|
353
|
+
return PrecursorVerdict(
|
|
354
|
+
stance=EvidenceStance.ATTESTED,
|
|
355
|
+
mutating_tool=call.tool_name,
|
|
356
|
+
required=tuple(sorted(required)),
|
|
357
|
+
present=tuple(present),
|
|
358
|
+
reason=(
|
|
359
|
+
f"mandated precursor(s) {present} fired before {call.tool_name!r} — "
|
|
360
|
+
f"the required lookup is present in the stream"
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
return PrecursorVerdict(
|
|
364
|
+
stance=EvidenceStance.REFUTED,
|
|
365
|
+
mutating_tool=call.tool_name,
|
|
366
|
+
required=tuple(sorted(required)),
|
|
367
|
+
present=(),
|
|
368
|
+
reason=(
|
|
369
|
+
f"{call.tool_name!r} is about to mutate, but none of its mandated precursor(s) "
|
|
370
|
+
f"{sorted(required)} produced a result in the stream — the required lookup was "
|
|
371
|
+
f"skipped (Missing Prerequisite Lookup); re-surface the requirement"
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# The intervention map — a DIRECT stance→rung map (the tool_stream precedent).
|
|
378
|
+
# NOT choose_intervention: that is ProvenanceVerdict-typed and reads fields a
|
|
379
|
+
# PrecursorVerdict does not carry. The only fired output is WARN — there is no
|
|
380
|
+
# harder rung in this map, no ceiling knob, no InterventionPolicy clamp, so BLOCK
|
|
381
|
+
# is unreachable BY CONSTRUCTION (the docs/147 §4 WARN-only-by-output-type guarantee).
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
def precursor_intervention(verdict: PrecursorVerdict) -> Optional[InterventionDecision]:
|
|
384
|
+
"""Map a `PrecursorVerdict` directly to an intervention rung. PURE + ADVISORY.
|
|
385
|
+
|
|
386
|
+
The mapping is a two-line literal — the `tool_stream` precedent (that leaf's consumer maps
|
|
387
|
+
its `StreamState` to a WARN without calling `choose_intervention`):
|
|
388
|
+
|
|
389
|
+
REFUTED -> Intervention.WARN (re-surface the mandated-precursor requirement)
|
|
390
|
+
ATTESTED/NO_SIGNAL -> None (no intervention; dispatch unchanged)
|
|
391
|
+
|
|
392
|
+
Returns `None` (not OBSERVE) on the non-fired stances so a consumer can `if decision:`
|
|
393
|
+
cheaply. The fired rung is **always WARN** — there is no rung above it in this map, no
|
|
394
|
+
confidence to assess (a `PrecursorVerdict` carries none), and no policy object to re-tune,
|
|
395
|
+
so a BLOCK/DEFER is unreachable for this signal by construction. "Mandated precursor absent"
|
|
396
|
+
cannot honestly carry a BLOCK's confidence (you cannot prove a check was *required for this
|
|
397
|
+
specific action* without the resource/clause relation the dead-line cut), and the wiring
|
|
398
|
+
reflects that: a fixed WARN, full stop. The kernel RECOMMENDS this; the consumer ACTS on it.
|
|
399
|
+
"""
|
|
400
|
+
if verdict.stance is not EvidenceStance.REFUTED:
|
|
401
|
+
return None
|
|
402
|
+
miss = ", ".join(verdict.required) or "a mandated lookup"
|
|
403
|
+
return InterventionDecision(
|
|
404
|
+
intervention=Intervention.WARN,
|
|
405
|
+
# The fields below are echoed for the InterventionDecision shape; a PrecursorVerdict has
|
|
406
|
+
# no Confidence, so we carry NONE (the honest "no confidence rung for this verdict type")
|
|
407
|
+
# and the precursor tool as the single "unsupported" subject the WARN names.
|
|
408
|
+
confidence=_no_confidence(),
|
|
409
|
+
rung=_warn_spec(),
|
|
410
|
+
disruption_cost=0.0,
|
|
411
|
+
unsupported=(verdict.mutating_tool,),
|
|
412
|
+
reason=(
|
|
413
|
+
f"mandated precursor absent for {verdict.mutating_tool!r} (required: {miss}) "
|
|
414
|
+
f"-> WARN: re-surface the requirement, dispatch preserved (turn not lost)"
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _no_confidence():
|
|
420
|
+
"""The `Confidence.NONE` literal — a `PrecursorVerdict` has no mint-confidence rung, so the
|
|
421
|
+
decision carries NONE honestly (never a fabricated HIGH that a downstream reader might
|
|
422
|
+
escalate on). Imported lazily to keep the module's import surface minimal."""
|
|
423
|
+
from dos.intervention import Confidence
|
|
424
|
+
return Confidence.NONE
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _warn_spec():
|
|
428
|
+
"""The shipped WARN `InterventionSpec` from the base ladder — reused, never re-declared, so
|
|
429
|
+
the rung's `dispatches`/`rank`/`actuation` data stays single-sourced (the consumer reads
|
|
430
|
+
`rung.dispatches` to know the call still fires)."""
|
|
431
|
+
from dos.intervention import BASE_INTERVENTIONS
|
|
432
|
+
spec = BASE_INTERVENTIONS.get("WARN")
|
|
433
|
+
if spec is None: # pragma: no cover - WARN is a base rung
|
|
434
|
+
raise KeyError("WARN is not in BASE_INTERVENTIONS")
|
|
435
|
+
return spec
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# The declarative on-ramp — read a grammar out of dos.toml (mirror tool_stream/intervention).
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
def grammar_from_table(table: dict) -> PrecursorGrammar:
|
|
442
|
+
"""Turn a parsed `[precursor]` TOML table into a `PrecursorGrammar`. PURE (no I/O).
|
|
443
|
+
|
|
444
|
+
`table` is `{requires: {tool: [precursor, ...] | precursor}, aliases: {precursor:
|
|
445
|
+
[alias, ...] | alias}, case_sensitive?}` — the shape `tomllib.load(...)["precursor"]`
|
|
446
|
+
yields (`[precursor.requires]` / `[precursor.aliases]` sub-tables). Names are canonicalized
|
|
447
|
+
at load so lookups need no per-call normalization. A scalar value is accepted in place of a
|
|
448
|
+
one-element list (the `see_also` / `ignore_tools` single-string convenience). Missing →
|
|
449
|
+
EMPTY_GRAMMAR (the gate NO_SIGNALs everything = today's behavior). PURE.
|
|
450
|
+
"""
|
|
451
|
+
if not table:
|
|
452
|
+
return EMPTY_GRAMMAR
|
|
453
|
+
|
|
454
|
+
def _as_tuple(v) -> tuple:
|
|
455
|
+
if v is None:
|
|
456
|
+
return ()
|
|
457
|
+
if isinstance(v, str):
|
|
458
|
+
return (v,)
|
|
459
|
+
return tuple(v)
|
|
460
|
+
|
|
461
|
+
requires_raw = table.get("requires", {}) or {}
|
|
462
|
+
aliases_raw = table.get("aliases", {}) or {}
|
|
463
|
+
requires = {
|
|
464
|
+
_canon(tool): tuple(_canon(p) for p in _as_tuple(precs))
|
|
465
|
+
for tool, precs in requires_raw.items()
|
|
466
|
+
}
|
|
467
|
+
aliases = {
|
|
468
|
+
_canon(prec): tuple(_canon(a) for a in _as_tuple(als))
|
|
469
|
+
for prec, als in aliases_raw.items()
|
|
470
|
+
}
|
|
471
|
+
return PrecursorGrammar(requires=requires, aliases=aliases)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def load_from_toml(
|
|
475
|
+
path: "Path | str", *, base: PrecursorGrammar = EMPTY_GRAMMAR
|
|
476
|
+
) -> PrecursorGrammar:
|
|
477
|
+
"""Build a `PrecursorGrammar` from a `dos.toml`'s `[precursor]` table.
|
|
478
|
+
|
|
479
|
+
Returns `base` unchanged when the file is absent, has no `[precursor]` table, or `tomllib`
|
|
480
|
+
is unavailable — the declarative path is purely additive, so a missing/empty config degrades
|
|
481
|
+
to the empty grammar (the gate NO_SIGNALs everything), never an error. A *present* table
|
|
482
|
+
REPLACES the grammar wholesale (the `[stamp]`/`[tool_stream]` override shape). Reads with
|
|
483
|
+
`utf-8-sig` to strip a PowerShell-written BOM (the `intervention.load_from_toml` fix).
|
|
484
|
+
"""
|
|
485
|
+
p = Path(path)
|
|
486
|
+
if not p.exists():
|
|
487
|
+
return base
|
|
488
|
+
try:
|
|
489
|
+
import tomllib # py3.11+
|
|
490
|
+
except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
|
|
491
|
+
try:
|
|
492
|
+
import tomli as tomllib # type: ignore
|
|
493
|
+
except ModuleNotFoundError:
|
|
494
|
+
return base
|
|
495
|
+
data = tomllib.loads(p.read_text(encoding="utf-8-sig"))
|
|
496
|
+
table = data.get("precursor")
|
|
497
|
+
if not isinstance(table, dict) or not table:
|
|
498
|
+
return base
|
|
499
|
+
return grammar_from_table(table)
|