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/exec_capability.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""XCAP — the arbitrary-code-execution capability classifier: *a SHAPE, not a word.*
|
|
2
|
+
|
|
3
|
+
docs/224 — idea **B1** from the Claude Code source audit (docs/189). The audit
|
|
4
|
+
found CC's `dangerousPatterns.ts`: a list that identifies which *allow-rule
|
|
5
|
+
prefixes* hand the model **arbitrary code execution** — `python`, `node`, `bash`,
|
|
6
|
+
`ssh`, `npx`, `eval`, … An allow-rule like `Bash(python:*)` is not "a python
|
|
7
|
+
command"; it is *a way to run anything at all*, because `python -c '<any code>'`
|
|
8
|
+
escapes every narrower gate. CC strips such rules at auto-mode entry. The insight
|
|
9
|
+
DOS lifts is the docs/158 law — **a capability is a SHAPE, not a word** — applied to
|
|
10
|
+
*command auditing* rather than output classification: you do not scan a command for
|
|
11
|
+
the substring "dangerous"; you ask "does the program it invokes grant arbitrary
|
|
12
|
+
execution?", matched against a closed, declared capability list.
|
|
13
|
+
|
|
14
|
+
This is a **pure classifier leaf**, the `terminal_error`/`arg_provenance` detector
|
|
15
|
+
shape (a pure verdict over already-gathered bytes), NOT an admission predicate. The
|
|
16
|
+
distinction is deliberate and load-bearing:
|
|
17
|
+
|
|
18
|
+
* `self_modify` is an *admission* predicate — it answers "may this LANE (a
|
|
19
|
+
file-tree request) be leased?" over a tree. It plugs into the arbiter's
|
|
20
|
+
conjunction.
|
|
21
|
+
* XCAP answers "does this COMMAND grant arbitrary exec?" over a command string.
|
|
22
|
+
DOS has no permission-rule allow-list surface (CC's home for this), so XCAP is
|
|
23
|
+
not an arbiter predicate — it is a classifier the **consumer** (`pretool_sensor`,
|
|
24
|
+
the PRE-moment PEP, docs/191) consults to attach an advisory signal to a
|
|
25
|
+
proposed Bash call. The verdict REPORTS the capability; the consumer decides
|
|
26
|
+
what to do with it (today: a WARN on the intervention ladder; a host driver may
|
|
27
|
+
escalate). Advisory by default — the docs/143 −9 pp lesson: spurious disruption
|
|
28
|
+
is the expensive mistake, so a capability *observation* never auto-denies on its
|
|
29
|
+
own (a deny is a host's explicit, --force-overridable choice).
|
|
30
|
+
|
|
31
|
+
**Byte-clean / SHAPE-not-word, made precise.** XCAP reads the *program token* — the
|
|
32
|
+
first word of the command (after stripping an `env VAR=…` / `sudo` prefix) — and
|
|
33
|
+
compares it to a closed set. It does NOT regex the whole command for scary
|
|
34
|
+
substrings (a path named `my_eval_helper.txt` is not an `eval`; a comment
|
|
35
|
+
mentioning `python` is not a python invocation). Matching the invoked-program SHAPE,
|
|
36
|
+
not a word anywhere in the string, is what keeps it from the forgeable-keyword trap
|
|
37
|
+
the docs/158 detector-design guide warns against. The command bytes are the agent's
|
|
38
|
+
*proposal* (agent-authored), so XCAP is a check on a PROPOSED capability, not a
|
|
39
|
+
distrust-of-result verdict — it belongs at PRE (before the call runs), exactly where
|
|
40
|
+
`pretool_sensor` already lives.
|
|
41
|
+
|
|
42
|
+
**Domain-free / mechanism-policy split.** The mechanism is "tokenize the command,
|
|
43
|
+
look up the program in the capability set." The policy — *which programs grant
|
|
44
|
+
arbitrary exec* — is data, defaulted to CC's `CROSS_PLATFORM_CODE_EXEC` list and
|
|
45
|
+
declarable per-workspace in `dos.toml [exec_capability]`. A host that ships an
|
|
46
|
+
internal interpreter adds one line of data; the kernel's matching logic never
|
|
47
|
+
changes. The classifier never branches on a host name.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import enum
|
|
53
|
+
from dataclasses import dataclass, field
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Capability(str, enum.Enum):
|
|
57
|
+
"""What execution capability a command grants — the typed verdict.
|
|
58
|
+
|
|
59
|
+
`str`-valued so it round-trips through a CLI stdout token / exit-code map
|
|
60
|
+
(the `liveness.Liveness` / `breaker.BreakerState` idiom).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
GRANTS_ARBITRARY_EXEC = "GRANTS_ARBITRARY_EXEC" # invokes an interpreter/shell/remote-exec
|
|
64
|
+
BOUNDED = "BOUNDED" # the invoked program is not a known arbitrary-exec entry point
|
|
65
|
+
EMPTY = "EMPTY" # no program token to classify (blank command)
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
68
|
+
return self.value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# The capability set — CC's CROSS_PLATFORM_CODE_EXEC, lifted verbatim. Each entry
|
|
73
|
+
# is a PROGRAM whose mere invocation grants arbitrary code execution (an
|
|
74
|
+
# interpreter that takes `-c`/`-e`, a shell, a package-runner that runs scripts, a
|
|
75
|
+
# remote-exec wrapper). This is the SHAPE the classifier matches the program token
|
|
76
|
+
# against — declared as data so a host extends it in `dos.toml [exec_capability]`,
|
|
77
|
+
# the closed-config-as-data pattern (`[lanes]`/`[reasons]`/`[liveness]`).
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
CROSS_PLATFORM_CODE_EXEC: frozenset[str] = frozenset({
|
|
80
|
+
# Interpreters — each takes inline code (`python -c`, `node -e`, `ruby -e`, …).
|
|
81
|
+
"python", "python3", "python2", "node", "deno", "tsx", "ruby", "perl", "php", "lua",
|
|
82
|
+
# Package runners — run arbitrary project scripts / fetched packages.
|
|
83
|
+
"npx", "bunx", "npm", "yarn", "pnpm", "bun",
|
|
84
|
+
# Shells — the most direct arbitrary-exec entry point.
|
|
85
|
+
"bash", "sh", "zsh", "fish",
|
|
86
|
+
# Exec built-ins / wrappers that run an arbitrary argument as a program.
|
|
87
|
+
"eval", "exec", "xargs",
|
|
88
|
+
# Remote / privilege wrappers — arbitrary command on another host / as root.
|
|
89
|
+
"ssh", "sudo",
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# Prefix tokens that wrap a REAL program without being the capability themselves:
|
|
93
|
+
# `env FOO=bar python …` and `sudo python …` both invoke `python`. We strip these
|
|
94
|
+
# (and any `VAR=value` assignments) to find the program token actually invoked.
|
|
95
|
+
# NOTE `sudo` is ALSO in the capability set (it grants root) — so `sudo rm` is
|
|
96
|
+
# GRANTS_ARBITRARY_EXEC via the sudo entry, while `sudo python` is caught either
|
|
97
|
+
# way; stripping it lets us also see the wrapped `python`. The verdict fires on the
|
|
98
|
+
# FIRST capability hit, so wrapping never hides a capability.
|
|
99
|
+
_WRAPPER_TOKENS: frozenset[str] = frozenset({"env", "sudo", "command", "nice", "nohup", "time"})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class ExecCapabilityPolicy:
|
|
104
|
+
"""The capability set to match against — policy, not mechanism.
|
|
105
|
+
|
|
106
|
+
The mechanism (tokenize, strip wrappers, look up) is the kernel's; the SET of
|
|
107
|
+
arbitrary-exec programs is data. Defaults to `CROSS_PLATFORM_CODE_EXEC` (CC's
|
|
108
|
+
list); a workspace declares additions in `dos.toml [exec_capability]`
|
|
109
|
+
(`extra = ["myinterp"]`), the same closed-config-as-data on-ramp as `[reasons]`.
|
|
110
|
+
|
|
111
|
+
programs — the closed set of program tokens that grant arbitrary execution.
|
|
112
|
+
Matched case-insensitively against the invoked program token (a
|
|
113
|
+
program's basename, lower-cased — `/usr/bin/python3` → `python3`).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
programs: frozenset[str] = field(default_factory=lambda: CROSS_PLATFORM_CODE_EXEC)
|
|
117
|
+
|
|
118
|
+
def with_extra(self, extra) -> "ExecCapabilityPolicy":
|
|
119
|
+
"""A new policy with `extra` program tokens added (the host on-ramp)."""
|
|
120
|
+
more = frozenset(str(p).strip().lower() for p in (extra or ()) if str(p).strip())
|
|
121
|
+
return ExecCapabilityPolicy(programs=self.programs | more)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
DEFAULT_POLICY = ExecCapabilityPolicy()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class ExecCapabilityVerdict:
|
|
129
|
+
"""The classifier's verdict + the evidence (the matched program), echoed back.
|
|
130
|
+
|
|
131
|
+
`capability` is the typed `Capability`. `program` is the invoked program token
|
|
132
|
+
the classifier extracted (the basename, lower-cased) — None for an empty
|
|
133
|
+
command. `reason` is the one-line operator-facing summary. The matched program
|
|
134
|
+
is carried so a consumer can name *what* grants the capability (legible
|
|
135
|
+
distrust — "GRANTS_ARBITRARY_EXEC via `python`", not a bare flag).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
capability: Capability
|
|
139
|
+
program: str | None
|
|
140
|
+
reason: str
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def grants_arbitrary_exec(self) -> bool:
|
|
144
|
+
return self.capability is Capability.GRANTS_ARBITRARY_EXEC
|
|
145
|
+
|
|
146
|
+
def to_dict(self) -> dict:
|
|
147
|
+
return {
|
|
148
|
+
"capability": self.capability.value,
|
|
149
|
+
"program": self.program,
|
|
150
|
+
"reason": self.reason,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _program_token(command: str) -> str | None:
|
|
155
|
+
"""Extract the program token a command invokes. PURE — a tokenizer, not a shell.
|
|
156
|
+
|
|
157
|
+
The SHAPE extraction: split off the first word, skipping leading `VAR=value`
|
|
158
|
+
assignments and benign wrappers (`env`, `nice`, `time`, …) to find the program
|
|
159
|
+
that is actually run. Returns the program's BASENAME, lower-cased
|
|
160
|
+
(`/usr/bin/python3` → `python3`, `PYTHON` → `python`), or None for a blank
|
|
161
|
+
command. Deliberately simple — it reads the invoked-program shape, never the
|
|
162
|
+
whole command (matching a word anywhere would be the forgeable-keyword trap).
|
|
163
|
+
"""
|
|
164
|
+
if not command or not command.strip():
|
|
165
|
+
return None
|
|
166
|
+
# Walk leading tokens, skipping VAR=value assignments and wrapper words, until
|
|
167
|
+
# we hit the real program. A wrapper that is ALSO a capability (`sudo`) is
|
|
168
|
+
# handled by the caller scanning all leading capability hits — here we just find
|
|
169
|
+
# the first non-wrapper, non-assignment token to report as the program.
|
|
170
|
+
for raw in command.strip().split():
|
|
171
|
+
tok = raw.strip()
|
|
172
|
+
if not tok:
|
|
173
|
+
continue
|
|
174
|
+
if "=" in tok and not tok.startswith("="):
|
|
175
|
+
# A leading `VAR=value` assignment (only before the program) — skip.
|
|
176
|
+
head = tok.split("=", 1)[0]
|
|
177
|
+
if head and all(c.isalnum() or c == "_" for c in head):
|
|
178
|
+
continue
|
|
179
|
+
base = tok.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
180
|
+
if base in _WRAPPER_TOKENS:
|
|
181
|
+
continue # a wrapper — keep walking to the wrapped program
|
|
182
|
+
return base
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _leading_tokens(command: str) -> list[str]:
|
|
187
|
+
"""The leading program-ish tokens (basenames, lower-cased) up to the first
|
|
188
|
+
non-wrapper program, INCLUDING any wrapper that is itself a capability. PURE.
|
|
189
|
+
|
|
190
|
+
Used so a command whose WRAPPER is a capability (`sudo rm`) fires on the sudo
|
|
191
|
+
entry even though the reported program is the wrapped `rm`. Returns the wrappers
|
|
192
|
+
seen plus the final program token, in order.
|
|
193
|
+
"""
|
|
194
|
+
out: list[str] = []
|
|
195
|
+
for raw in command.strip().split():
|
|
196
|
+
tok = raw.strip()
|
|
197
|
+
if not tok:
|
|
198
|
+
continue
|
|
199
|
+
if "=" in tok and not tok.startswith("="):
|
|
200
|
+
head = tok.split("=", 1)[0]
|
|
201
|
+
if head and all(c.isalnum() or c == "_" for c in head):
|
|
202
|
+
continue
|
|
203
|
+
base = tok.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
204
|
+
out.append(base)
|
|
205
|
+
if base not in _WRAPPER_TOKENS:
|
|
206
|
+
break # reached the real program — stop
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def classify_command(
|
|
211
|
+
command: str, policy: ExecCapabilityPolicy = DEFAULT_POLICY
|
|
212
|
+
) -> ExecCapabilityVerdict:
|
|
213
|
+
"""Classify the execution capability a command grants. PURE — no I/O.
|
|
214
|
+
|
|
215
|
+
The ladder:
|
|
216
|
+
1. EMPTY — no program token (a blank command). Nothing to classify.
|
|
217
|
+
2. GRANTS_ARBITRARY_EXEC — the invoked program (or a capability wrapper like
|
|
218
|
+
`sudo` in front of it) is in the capability set. `python -c …`, `bash -c …`,
|
|
219
|
+
`npx …`, `ssh host …`, `sudo …` — each is a way to run arbitrary code.
|
|
220
|
+
3. BOUNDED — the invoked program is not a known arbitrary-exec entry point
|
|
221
|
+
(`ls`, `cat`, `git status`, `grep`, …). This is NOT a safety guarantee
|
|
222
|
+
(`git` can run hooks; the audit notes `git`/`gh`/`curl` are ant-only
|
|
223
|
+
additions) — only "not a member of the declared arbitrary-exec set." A host
|
|
224
|
+
that wants those flagged adds them to `[exec_capability]`.
|
|
225
|
+
|
|
226
|
+
Matches the SHAPE (the invoked program), never a substring of the command — so a
|
|
227
|
+
file named `eval.txt` or a comment mentioning `python` does not trip it.
|
|
228
|
+
"""
|
|
229
|
+
program = _program_token(command)
|
|
230
|
+
if program is None:
|
|
231
|
+
return ExecCapabilityVerdict(
|
|
232
|
+
capability=Capability.EMPTY,
|
|
233
|
+
program=None,
|
|
234
|
+
reason="empty command — no program token to classify",
|
|
235
|
+
)
|
|
236
|
+
# Scan the leading tokens (wrappers + the program) for the FIRST capability hit,
|
|
237
|
+
# so `sudo python` / `env X=1 bash` fire on whichever capability appears.
|
|
238
|
+
for tok in _leading_tokens(command):
|
|
239
|
+
if tok in policy.programs:
|
|
240
|
+
return ExecCapabilityVerdict(
|
|
241
|
+
capability=Capability.GRANTS_ARBITRARY_EXEC,
|
|
242
|
+
program=program,
|
|
243
|
+
reason=(
|
|
244
|
+
f"the command invokes {tok!r}, an arbitrary-code-execution entry "
|
|
245
|
+
f"point — it can run any code, escaping a narrower per-command gate "
|
|
246
|
+
f"(GRANTS_ARBITRARY_EXEC)"
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
return ExecCapabilityVerdict(
|
|
250
|
+
capability=Capability.BOUNDED,
|
|
251
|
+
program=program,
|
|
252
|
+
reason=(
|
|
253
|
+
f"the command invokes {program!r}, not a known arbitrary-exec entry point "
|
|
254
|
+
f"— bounded (NOT a safety guarantee; only 'not in the declared exec set')"
|
|
255
|
+
),
|
|
256
|
+
)
|
dos/export_cursor.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""export_cursor — the resumable drain offset for `dos export` (docs/266 Phase 4).
|
|
2
|
+
|
|
3
|
+
The verdict exporter (`dos.exporter` + the `file`/`statsd`/`otlp` drivers) drains the
|
|
4
|
+
verdict journal forward; the cursor is *how far it got*. It is the journal's OWN
|
|
5
|
+
monotonic `seq` (nothing fabricated — the docs/262 spine key, the `--since` offset
|
|
6
|
+
Phases 1–3 already carry on every `ExportResult.cursor`), persisted to a tiny file so a
|
|
7
|
+
repeated `dos export` auto-resumes WITHOUT the operator threading the number forward by
|
|
8
|
+
hand.
|
|
9
|
+
|
|
10
|
+
Why a separate module, not `exporter.py`
|
|
11
|
+
=========================================
|
|
12
|
+
|
|
13
|
+
`exporter.py` is the kernel SEAM — a pure Protocol + resolver + fail-soft wrapper, the
|
|
14
|
+
`notify.py` shape, deliberately import-light and (apart from entry-point discovery)
|
|
15
|
+
I/O-free. The cursor is WAL-ADJACENT STATE — a one-line file read/written at the drain
|
|
16
|
+
boundary, exactly the kind of thing `verdict_journal.py` (a substrate data module) owns,
|
|
17
|
+
not the pure seam. So it lives here, resolved as a sibling of the verdict journal (the
|
|
18
|
+
`verdict_journal._default_journal_path` idiom), with the same fail-soft posture: a read
|
|
19
|
+
that cannot parse returns 0 (drain from the start), a write that fails is swallowed (a
|
|
20
|
+
cursor-persistence failure must never crash the drain — the `verdict_journal.record`
|
|
21
|
+
contract, inherited).
|
|
22
|
+
|
|
23
|
+
Host-cadence-free (the kernel ships no daemon)
|
|
24
|
+
==============================================
|
|
25
|
+
|
|
26
|
+
The cursor makes the drain RESUMABLE; it does NOT make it a daemon. A fleet drives the
|
|
27
|
+
*cadence* — `dos export --to file --since auto` on a `/loop`/cron tick reads the cursor,
|
|
28
|
+
ships the new tail, writes the cursor back. The kernel owns the OFFSET, the host owns the
|
|
29
|
+
CLOCK (the `dos notify` / `dos top` posture: no `while True` in the kernel). The
|
|
30
|
+
`--follow` convenience verb is a BOUNDED foreground loop (it always terminates on a max
|
|
31
|
+
iteration / a quiet streak), never an unbounded blocker.
|
|
32
|
+
|
|
33
|
+
The file is `.dos/export-cursor` (docs/266 §4) — a sibling of the journal it tracks, one
|
|
34
|
+
line: the highest `seq` shipped. A per-transport suffix keeps two destinations
|
|
35
|
+
(a file shipper + an OTLP collector) from clobbering each other's progress.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from dos import config as _config
|
|
44
|
+
|
|
45
|
+
# The workspace-neutral env override (parallel to DISPATCH_VERDICT_JOURNAL_PATH). Points
|
|
46
|
+
# at the cursor FILE (or its stem when a per-transport suffix is appended).
|
|
47
|
+
_ENV_PATH = "DISPATCH_EXPORT_CURSOR_PATH"
|
|
48
|
+
|
|
49
|
+
# The sentinel a CLI passes for `--since` to mean "read the persisted cursor" rather than
|
|
50
|
+
# an explicit integer. Kept here so the verb and the helpers agree on the spelling.
|
|
51
|
+
AUTO = "auto"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _default_cursor_path() -> Path:
|
|
55
|
+
"""The active workspace's export-cursor file — `.dos/export-cursor`, a journal sibling.
|
|
56
|
+
|
|
57
|
+
Mirrors `verdict_journal._default_journal_path`: derive it from the resolved verdict
|
|
58
|
+
journal (so a `DISPATCH_VERDICT_JOURNAL_PATH` redirect carries the cursor along), or
|
|
59
|
+
fall back to a lane-journal sibling when the layout field is unset."""
|
|
60
|
+
paths = _config.active().paths
|
|
61
|
+
vj = getattr(paths, "verdict_journal", None)
|
|
62
|
+
base = Path(vj) if vj is not None else Path(paths.lane_journal).with_name(
|
|
63
|
+
"verdict-journal.jsonl")
|
|
64
|
+
return base.with_name("export-cursor")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cursor_path(path: Path | None = None, *, transport: str = "") -> Path:
|
|
68
|
+
"""Resolve the cursor file: explicit arg › env override › `.dos/export-cursor`.
|
|
69
|
+
|
|
70
|
+
`transport` (when given) is appended as a `.<transport>` suffix so distinct
|
|
71
|
+
destinations track independent progress — `.dos/export-cursor.file` vs
|
|
72
|
+
`.dos/export-cursor.otlp` — and a `dos export --to file` drain never advances the
|
|
73
|
+
cursor an OTLP drain reads. Re-read each call so a test that sets the env var after
|
|
74
|
+
import still redirects (the lane-journal idiom)."""
|
|
75
|
+
if path is not None:
|
|
76
|
+
base = Path(path)
|
|
77
|
+
else:
|
|
78
|
+
env = os.environ.get(_ENV_PATH)
|
|
79
|
+
base = Path(env) if env else _default_cursor_path()
|
|
80
|
+
if transport:
|
|
81
|
+
return base.with_name(f"{base.name}.{transport}")
|
|
82
|
+
return base
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_cursor(path: Path | None = None, *, transport: str = "") -> int:
|
|
86
|
+
"""The persisted cursor (highest seq shipped), or 0 when none/unreadable. FAIL-SOFT.
|
|
87
|
+
|
|
88
|
+
A missing file, an empty file, or a non-integer body all return 0 — "drain from the
|
|
89
|
+
start," the safe default (re-shipping a few events is harmless and idempotent for a
|
|
90
|
+
file/statsd/otlp sink; never advancing past unread events is the failure to avoid).
|
|
91
|
+
Never raises (the `verdict_journal.read_all` posture)."""
|
|
92
|
+
p = cursor_path(path, transport=transport)
|
|
93
|
+
try:
|
|
94
|
+
raw = p.read_text(encoding="utf-8").strip()
|
|
95
|
+
except OSError:
|
|
96
|
+
return 0
|
|
97
|
+
if not raw:
|
|
98
|
+
return 0
|
|
99
|
+
try:
|
|
100
|
+
return int(raw)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def write_cursor(value: int, path: Path | None = None, *, transport: str = "") -> bool:
|
|
106
|
+
"""Persist `value` as the new cursor. Returns True on success, False on failure. FAIL-SOFT.
|
|
107
|
+
|
|
108
|
+
The dir is created on demand (`mkdir(parents=True)`, like the journal writers). A
|
|
109
|
+
write failure (full disk, permission) is swallowed and reported as False — a
|
|
110
|
+
cursor-persistence failure must never crash the drain that produced the events (the
|
|
111
|
+
`verdict_journal.record` fail-soft contract). A negative/zero value is written as-is
|
|
112
|
+
(0 is the honest "nothing shipped yet" cursor)."""
|
|
113
|
+
p = cursor_path(path, transport=transport)
|
|
114
|
+
try:
|
|
115
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
p.write_text(f"{int(value)}\n", encoding="utf-8")
|
|
117
|
+
return True
|
|
118
|
+
except Exception:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_since(since_arg: str, *, path: Path | None = None, transport: str = "") -> tuple[int, bool]:
|
|
123
|
+
"""Turn a `--since` value into (seq, auto). Pure-ish (reads the cursor only for AUTO).
|
|
124
|
+
|
|
125
|
+
Returns `(seq, auto)` where `auto` is True iff the operator passed the `AUTO` sentinel
|
|
126
|
+
(so the verb knows to WRITE the cursor back after a successful drain). The mapping:
|
|
127
|
+
|
|
128
|
+
* "" / missing → (0, False) — no slice, drain everything; do NOT persist
|
|
129
|
+
* "auto" → (read_cursor(), True) — resume from the persisted cursor,
|
|
130
|
+
and persist the new high-water mark after the drain
|
|
131
|
+
* an integer string → (int, False) — explicit one-shot offset; do NOT persist
|
|
132
|
+
|
|
133
|
+
A non-integer, non-`auto` value raises ValueError (an operator typo, surfaced at the
|
|
134
|
+
boundary — the `resolve_notifier` loud-on-bad-input rule). So a `/loop` runs
|
|
135
|
+
`dos export --since auto` and the cursor threads itself; a human debugging runs
|
|
136
|
+
`--since 42` for a one-shot without disturbing the persisted offset.
|
|
137
|
+
"""
|
|
138
|
+
s = (since_arg or "").strip()
|
|
139
|
+
if not s:
|
|
140
|
+
return (0, False)
|
|
141
|
+
if s.lower() == AUTO:
|
|
142
|
+
return (read_cursor(path, transport=transport), True)
|
|
143
|
+
return (int(s), False) # raises ValueError on a bad token — caught at the boundary
|