From 68294e9db3db4411b6ed61bb43b2cb9d80e0ca5d Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:51:49 +0200 Subject: [PATCH] fix(learn): v1 also withholds function length (a leak by construction) The leak audit found facts.function_lines separating fixed from vulnerable pair sides (a fix guard lengthens the function: 0.80 untrusted_destination, 0.75 config_secrets). Input profiles may now name single features; v1 withholds the engine and the function length from prompts, retrieval and the proposer summary. A withheld feature keeps its instrument block in the distance (state still counts). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- src/openultrasast/learn/compile.py | 3 ++- src/openultrasast/learn/program.py | 2 +- src/openultrasast/learn/retrieve.py | 6 ++++-- src/openultrasast/learn/schema.py | 5 ++++- tests/test_learn_leaks.py | 21 +++++++++++++++++++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/openultrasast/learn/compile.py b/src/openultrasast/learn/compile.py index 765286e..aa340a8 100644 --- a/src/openultrasast/learn/compile.py +++ b/src/openultrasast/learn/compile.py @@ -157,7 +157,8 @@ def _signal_summary(example: Example, inputs: str = DEFAULT_INPUTS) -> dict[str, hidden = withheld(inputs) ran = { k: v for k, v in example.x.items() - if v not in (None, 0, False) and not k.startswith("lang.") and not (k in BY_NAME and BY_NAME[k].instrument in hidden) + if v not in (None, 0, False) and not k.startswith("lang.") and k not in hidden + and not (k in BY_NAME and BY_NAME[k].instrument in hidden) } # fmt: skip return dict(sorted(ran.items())[:12]) diff --git a/src/openultrasast/learn/program.py b/src/openultrasast/learn/program.py index 740a88c..777a455 100644 --- a/src/openultrasast/learn/program.py +++ b/src/openultrasast/learn/program.py @@ -118,7 +118,7 @@ def render_signals(x: Mapping[str, Any], instruments: Mapping[str, Mapping[str, lines.append(f"{name}: {STATE_WORDS[state]}") continue for spec in features_for(profile): - if spec.instrument == name: + if spec.instrument == name and spec.name not in hidden: value = x[spec.name] lines.append(f"{spec.name}: {'null' if value is None else json.dumps(value)}") return "\n".join(lines) diff --git a/src/openultrasast/learn/retrieve.py b/src/openultrasast/learn/retrieve.py index 4a6d538..8119f16 100644 --- a/src/openultrasast/learn/retrieve.py +++ b/src/openultrasast/learn/retrieve.py @@ -97,13 +97,15 @@ def gower( """The per-instrument Gower distance of two records of ``profile`` (module docstring), in [0, 1]. The blocks of instruments the input profile withholds (v1: the engine) are not compared, so neighbours cannot carry them.""" hidden = withheld(inputs) - total, blocks = 0.0, [(name, specs) for name, specs in _blocks(profile, priors) if name not in hidden] + # a withheld feature leaves its instrument's block in place: the state still counts, as it is still rendered + total = 0.0 + blocks = [(name, [s for s in specs if s.name not in hidden]) for name, specs in _blocks(profile, priors) if name not in hidden] for name, specs in blocks: sa, sb = a_instruments.get(name, {}).get("state", "none"), b_instruments.get(name, {}).get("state", "none") if sa != sb: total += 1.0 elif sa == "ran": - total += sum(_feature_distance(s, a_x.get(s.name), b_x.get(s.name)) for s in specs) / len(specs) + total += sum(_feature_distance(s, a_x.get(s.name), b_x.get(s.name)) for s in specs) / len(specs) if specs else 0.0 return total / len(blocks) if blocks else 0.0 diff --git a/src/openultrasast/learn/schema.py b/src/openultrasast/learn/schema.py index a924393..3a48e5d 100644 --- a/src/openultrasast/learn/schema.py +++ b/src/openultrasast/learn/schema.py @@ -99,7 +99,10 @@ EXCLUDED_FIELDS = frozenset( # instruction proposer and the retrieval distance. On the pairs the engine's "read the file, asked no question" state # tracks the label (a fixed side loses its sink), so v1 withholds the engine entirely; it returns only through a # pre-registered A/B on negatives where it cannot track a fix. -INPUT_PROFILES: Mapping[str, tuple[str, ...]] = {"v1": ("engine",), "full": ()} +# Entries name an instrument (all its state and values) or a single feature. v1 withholds the engine and the function +# length: both separate fixed from vulnerable pair sides by construction (a fix removes a sink, a guard lengthens the +# function), not by anything that holds on unseen code (leak audit 2026-10-01). +INPUT_PROFILES: Mapping[str, tuple[str, ...]] = {"v1": ("engine", "facts.function_lines"), "full": ()} DEFAULT_INPUTS = "v1" diff --git a/tests/test_learn_leaks.py b/tests/test_learn_leaks.py index 978d504..a9b44a7 100644 --- a/tests/test_learn_leaks.py +++ b/tests/test_learn_leaks.py @@ -92,3 +92,24 @@ def test_drop_rows_removes_only_the_named_rows(tmp_path: Path) -> None: assert store.drop_rows("github.com/o/r", "0" * 40, ["u1v:a.py::g", "absent"]) == 1 assert [r.row["id"] for r in store.rows(kind="features")] == ["u1v:a.py::f"] assert store.drop_rows("github.com/o/r", "0" * 40, ["u1v:a.py::f"]) == 1 and store.rows(kind="features") == [] + + +def test_v1_withholds_function_length_from_prompts_and_distance() -> None: + """The leak audit (2026-10-01) found facts.function_lines separating pair sides by construction (a fix's guard + lengthens the function): v1 renders it nowhere and it cannot move the retrieval distance; ``full`` keeps it.""" + from learn_fixtures import x_record + + from openultrasast.learn.program import render_signals + from openultrasast.learn.retrieve import gower + from openultrasast.learn.schema import features_for + + a = x_record() + a["instruments"]["source"] = {"state": "ran"} + for spec in features_for("static"): + if spec.instrument == "source": + a["x"][spec.name] = 42 + b = {"x": {**a["x"], "facts.function_lines": 400}, "instruments": a["instruments"]} + assert "facts.function_lines" not in render_signals(a["x"], a["instruments"], "static") + assert "facts.function_lines: 42" in render_signals(a["x"], a["instruments"], "static", inputs="full") + assert gower(a["x"], a["instruments"], b["x"], b["instruments"], "static") == 0.0 + assert gower(a["x"], a["instruments"], b["x"], b["instruments"], "static", inputs="full") > 0.0