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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-10-01 19:51:49 +02:00
co-authored by Claude Opus 5.5
parent 9e3c83cfdc
commit 68294e9db3
5 changed files with 32 additions and 5 deletions
+2 -1
View File
@@ -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])
+1 -1
View File
@@ -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)
+4 -2
View File
@@ -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
+4 -1
View File
@@ -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"