stepcheck-dev 0.1.0__py3-none-any.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.
- backend/__init__.py +0 -0
- backend/audit.py +351 -0
- backend/cli.py +274 -0
- backend/kernel/__init__.py +614 -0
- backend/kernel/_gates.py +299 -0
- backend/kernel/_okf.py +663 -0
- backend/kernel/_sandbox.py +135 -0
- backend/kernel/_telemetry.py +52 -0
- backend/kernel/_tokens.py +7 -0
- backend/server.py +584 -0
- stepcheck_dev-0.1.0.dist-info/METADATA +123 -0
- stepcheck_dev-0.1.0.dist-info/RECORD +17 -0
- stepcheck_dev-0.1.0.dist-info/WHEEL +5 -0
- stepcheck_dev-0.1.0.dist-info/entry_points.txt +2 -0
- stepcheck_dev-0.1.0.dist-info/licenses/LICENSE +202 -0
- stepcheck_dev-0.1.0.dist-info/licenses/NOTICE +4 -0
- stepcheck_dev-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Isolated module namespace loading for callable invariants. Private.
|
|
2
|
+
|
|
3
|
+
`load_namespace()` exec-loads one module's staged source into a fresh dict.
|
|
4
|
+
Imports of *project* modules resolve to the kernel's canonical sources (never
|
|
5
|
+
to the files on disk, which only change once a commit has passed) through a temporary
|
|
6
|
+
meta-path finder; everything else imports normally. This is an isolated
|
|
7
|
+
namespace, not a security sandbox — invariants run the agent's own code.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.abc
|
|
12
|
+
import importlib.machinery
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
import types
|
|
16
|
+
|
|
17
|
+
_PREFIX = "_stepcheck_ns_"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _CanonicalFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
|
21
|
+
"""Serves project modules — flat or inside packages — from canonical sources."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, sources: dict[str, str], packages: set[str]):
|
|
24
|
+
self.sources = sources
|
|
25
|
+
self.packages = packages
|
|
26
|
+
|
|
27
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
28
|
+
if fullname in self.sources:
|
|
29
|
+
return importlib.machinery.ModuleSpec(
|
|
30
|
+
fullname, self, origin=f"<okf:{fullname}>", is_package=fullname in self.packages
|
|
31
|
+
)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def create_module(self, spec):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def exec_module(self, module):
|
|
38
|
+
origin = module.__spec__.origin
|
|
39
|
+
module.__file__ = origin
|
|
40
|
+
exec(compile(self.sources[module.__name__], origin, "exec"), module.__dict__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_namespace(
|
|
44
|
+
node_id: str,
|
|
45
|
+
source: str,
|
|
46
|
+
project_sources: dict[str, str],
|
|
47
|
+
project_paths: dict[str, str] | None = None,
|
|
48
|
+
) -> dict:
|
|
49
|
+
"""Return the module namespace of `source` with project imports resolved.
|
|
50
|
+
|
|
51
|
+
`project_paths` (node_id -> file path) turns node ids into import names
|
|
52
|
+
through _okf.module_map, so `pkg/__init__.py` loads as package `pkg`,
|
|
53
|
+
`from . import sibling` inside it resolves to the sibling's canonical
|
|
54
|
+
source, and source-root aliases work too: `from kernel import X` inside
|
|
55
|
+
`backend/server.py` binds the canonical `backend/kernel/__init__.py`, not
|
|
56
|
+
whatever sys.path holds. Parents without their own `__init__.py` become
|
|
57
|
+
empty namespace packages. Without paths every node is a flat top-level
|
|
58
|
+
module named after its id. Real modules that share a name are shadowed
|
|
59
|
+
only while loading and restored afterwards."""
|
|
60
|
+
from . import _okf
|
|
61
|
+
|
|
62
|
+
paths = dict(project_paths or {})
|
|
63
|
+
by_node = dict(project_sources)
|
|
64
|
+
by_node[node_id] = source
|
|
65
|
+
if paths:
|
|
66
|
+
names = _okf.module_map({nid: p for nid, p in paths.items() if nid in by_node})
|
|
67
|
+
else:
|
|
68
|
+
names = {nid: nid for nid in by_node}
|
|
69
|
+
sources = {name: by_node[nid] for name, nid in names.items()}
|
|
70
|
+
target = _okf.module_name(paths[node_id]) if node_id in paths else node_id
|
|
71
|
+
sources.setdefault(target, source)
|
|
72
|
+
inits = {
|
|
73
|
+
nid for nid, p in paths.items() if p.replace("\\", "/").rsplit("/", 1)[-1] == "__init__.py"
|
|
74
|
+
}
|
|
75
|
+
packages = {name for name, nid in names.items() if nid in inits}
|
|
76
|
+
for mod in list(sources):
|
|
77
|
+
parts = mod.split(".")
|
|
78
|
+
for i in range(1, len(parts)):
|
|
79
|
+
prefix = ".".join(parts[:i])
|
|
80
|
+
packages.add(prefix)
|
|
81
|
+
sources.setdefault(prefix, "")
|
|
82
|
+
|
|
83
|
+
def owned(modname: str) -> bool:
|
|
84
|
+
return modname in sources or any(modname.startswith(p + ".") for p in packages)
|
|
85
|
+
|
|
86
|
+
finder = _CanonicalFinder(sources, packages)
|
|
87
|
+
shadowed = {n: sys.modules.pop(n) for n in list(sys.modules) if owned(n)}
|
|
88
|
+
sys.meta_path.insert(0, finder)
|
|
89
|
+
try:
|
|
90
|
+
module = importlib.import_module(target)
|
|
91
|
+
return dict(module.__dict__)
|
|
92
|
+
finally:
|
|
93
|
+
sys.meta_path.remove(finder)
|
|
94
|
+
for n in list(sys.modules):
|
|
95
|
+
if owned(n):
|
|
96
|
+
del sys.modules[n]
|
|
97
|
+
sys.modules.update(shadowed)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# -------------------------------------------------------- execution guard
|
|
101
|
+
|
|
102
|
+
STEP_BUDGET = 200_000 # Python line events
|
|
103
|
+
WALL_BUDGET_SECONDS = 0.5 # fallback for work that emits few line events
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ExecutionBudgetExceeded(RuntimeError):
|
|
107
|
+
"""Raised inside the guarded code by the trace hook."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def guarded(fn, *, steps: int = STEP_BUDGET, seconds: float = WALL_BUDGET_SECONDS):
|
|
111
|
+
"""Run `fn()` under a per-thread trace hook that aborts at `steps` line
|
|
112
|
+
events or `seconds` of wall clock, whichever first. Deterministic on the
|
|
113
|
+
step axis; the clock is a backstop. Recursion depth is left to Python's
|
|
114
|
+
own RecursionError. Thread-local: sys.settrace affects this thread only."""
|
|
115
|
+
import time
|
|
116
|
+
|
|
117
|
+
deadline = time.monotonic() + seconds
|
|
118
|
+
count = 0
|
|
119
|
+
|
|
120
|
+
def tracer(frame, event, arg):
|
|
121
|
+
nonlocal count
|
|
122
|
+
if event == "line":
|
|
123
|
+
count += 1
|
|
124
|
+
if count > steps:
|
|
125
|
+
raise ExecutionBudgetExceeded(f"exceeded {steps:,} execution steps")
|
|
126
|
+
if time.monotonic() > deadline:
|
|
127
|
+
raise ExecutionBudgetExceeded(f"exceeded {seconds:.1f}s wall clock")
|
|
128
|
+
return tracer
|
|
129
|
+
|
|
130
|
+
previous = sys.gettrace()
|
|
131
|
+
sys.settrace(tracer)
|
|
132
|
+
try:
|
|
133
|
+
return fn()
|
|
134
|
+
finally:
|
|
135
|
+
sys.settrace(previous)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Telemetry math — the one place rate constants live (GLOSSARY: Telemetry terms).
|
|
2
|
+
|
|
3
|
+
Private implementation of StateKernel; reached only through get_telemetry().
|
|
4
|
+
Raw model: an unfirewalled agent re-reads the full source of every file touched
|
|
5
|
+
so far on every turn. A re-read more than CACHE_CLIFF_SECONDS after the previous
|
|
6
|
+
one bills at full input rate; inside the window it bills at CACHE_HIT_FRACTION.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
USD_PER_MTOK_INPUT = 3.00 # Sonnet-class input rate
|
|
13
|
+
CACHE_HIT_FRACTION = 0.1
|
|
14
|
+
CACHE_CLIFF_SECONDS = 300.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compute(events: list[dict]) -> tuple[int, int, int]:
|
|
18
|
+
"""-> (raw_bloated_tokens, compacted_okf_tokens, turns).
|
|
19
|
+
|
|
20
|
+
Events are the session's ordered checkout/commit records, each carrying
|
|
21
|
+
`node_id`, `tokens` (what the firewall actually sent), `full_tokens`
|
|
22
|
+
(the node's full-source size at that moment) and `at` (seconds; None means
|
|
23
|
+
"same instant as the previous event").
|
|
24
|
+
"""
|
|
25
|
+
raw = 0
|
|
26
|
+
compacted = 0
|
|
27
|
+
turns = 0
|
|
28
|
+
touched: dict[str, int] = {} # node_id -> latest full-source tokens
|
|
29
|
+
last_read: dict[str, float] = {}
|
|
30
|
+
clock = 0.0
|
|
31
|
+
|
|
32
|
+
for ev in events:
|
|
33
|
+
if ev["kind"] not in ("checkout", "commit"):
|
|
34
|
+
continue
|
|
35
|
+
turns += 1
|
|
36
|
+
at = ev["at"] if ev.get("at") is not None else clock
|
|
37
|
+
clock = at
|
|
38
|
+
compacted += ev["tokens"]
|
|
39
|
+
touched[ev["node_id"]] = ev["full_tokens"]
|
|
40
|
+
for node_id, full in touched.items():
|
|
41
|
+
prev = last_read.get(node_id)
|
|
42
|
+
if prev is None or (at - prev) > CACHE_CLIFF_SECONDS:
|
|
43
|
+
raw += full
|
|
44
|
+
else:
|
|
45
|
+
raw += math.ceil(full * CACHE_HIT_FRACTION)
|
|
46
|
+
last_read[node_id] = at
|
|
47
|
+
|
|
48
|
+
return raw, compacted, turns
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def dollars(tokens_saved: int) -> float:
|
|
52
|
+
return tokens_saved * USD_PER_MTOK_INPUT / 1_000_000
|