open-interpretability 0.0.20__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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Commands that vouch for records: ``reproduce``, ``sign`` and ``signatures``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from openinterp.cli import cli, wiki_dir_option
|
|
11
|
+
from openinterp.cli_memory import memory_option
|
|
12
|
+
from openinterp.core.paths import experiments_dir
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.command()
|
|
16
|
+
@click.argument("records", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
17
|
+
@click.option(
|
|
18
|
+
"--base-ref",
|
|
19
|
+
help="Reproduce every record added or changed since this git ref (e.g. origin/main).",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--sample",
|
|
23
|
+
type=click.IntRange(min=1),
|
|
24
|
+
help="Reproduce this many records of the registry, chosen in rotation by --date. Implies "
|
|
25
|
+
"--include-signed, and compares only the measurements of older protocols' records.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--date",
|
|
29
|
+
"sample_date",
|
|
30
|
+
type=click.DateTime(formats=["%Y-%m-%d"]),
|
|
31
|
+
help="The day that selects the --sample records (default: today, UTC).",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--time-budget",
|
|
35
|
+
type=click.FloatRange(min=0),
|
|
36
|
+
help="Minutes after which no new record is started. The records not reached are listed "
|
|
37
|
+
"and don't fail the run. Leave room for the slowest record within any outer time limit.",
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--include-signed",
|
|
41
|
+
is_flag=True,
|
|
42
|
+
help="Reproduce signed records too. A signature that doesn't verify still fails. A "
|
|
43
|
+
"signature that landed outside its key's validity window is reproduced anyway.",
|
|
44
|
+
)
|
|
45
|
+
@wiki_dir_option()
|
|
46
|
+
@click.option(
|
|
47
|
+
"--atol",
|
|
48
|
+
default=1e-3,
|
|
49
|
+
show_default=True,
|
|
50
|
+
type=click.FloatRange(min=0),
|
|
51
|
+
help="Absolute tolerance, as a fraction of the neuron's max activation.",
|
|
52
|
+
)
|
|
53
|
+
@click.option("--rtol", default=1e-2, show_default=True, type=click.FloatRange(min=0))
|
|
54
|
+
@memory_option
|
|
55
|
+
@click.option(
|
|
56
|
+
"--device",
|
|
57
|
+
default="auto",
|
|
58
|
+
show_default=True,
|
|
59
|
+
help="cpu, cuda, mps or auto (CUDA, else MPS, else CPU).",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--corpus",
|
|
63
|
+
"corpora",
|
|
64
|
+
multiple=True,
|
|
65
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
66
|
+
help="A scan corpus (repeatable): re-draw the corpus sample of each record that "
|
|
67
|
+
"scanned it, matched by SHA-256, and check it holds the top examples. The built-in "
|
|
68
|
+
"corpus is always known. Other records get a warning.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--allow-model",
|
|
72
|
+
"allowed_models",
|
|
73
|
+
multiple=True,
|
|
74
|
+
help="Only load these Hugging Face models (repeatable); records naming any other model "
|
|
75
|
+
"fail without a download. Default: any model.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--allowed-signers",
|
|
79
|
+
default=Path(".github/allowed_signers"),
|
|
80
|
+
show_default=True,
|
|
81
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
82
|
+
help="Records signed by a key listed here skip reproduction. With --base-ref the file "
|
|
83
|
+
"is read from that ref, so a branch can't add its own key.",
|
|
84
|
+
)
|
|
85
|
+
def reproduce(
|
|
86
|
+
records,
|
|
87
|
+
base_ref,
|
|
88
|
+
sample,
|
|
89
|
+
sample_date,
|
|
90
|
+
time_budget,
|
|
91
|
+
include_signed,
|
|
92
|
+
wiki_dir,
|
|
93
|
+
atol,
|
|
94
|
+
rtol,
|
|
95
|
+
device,
|
|
96
|
+
corpora,
|
|
97
|
+
allowed_models,
|
|
98
|
+
allowed_signers,
|
|
99
|
+
):
|
|
100
|
+
"""Re-run experiment records against their pinned model and compare (no LLM calls).
|
|
101
|
+
|
|
102
|
+
Each record's scan evidence, counterfactual deltas and review are measured again
|
|
103
|
+
with the model revision and dtype it pins, under its own review policy. Exits 1 if
|
|
104
|
+
any number differs beyond tolerance or any pass/fail, gate or status differs.
|
|
105
|
+
"""
|
|
106
|
+
import os
|
|
107
|
+
|
|
108
|
+
from openinterp.core.errata import load_errata
|
|
109
|
+
from openinterp.reproduce import Tolerance
|
|
110
|
+
from openinterp.signing import allowed_signers_text, landing_times
|
|
111
|
+
|
|
112
|
+
paths = _chosen_records(records, base_ref, sample, sample_date, wiki_dir)
|
|
113
|
+
if not paths:
|
|
114
|
+
click.echo("No records to reproduce.")
|
|
115
|
+
return
|
|
116
|
+
in_actions = os.environ.get("GITHUB_ACTIONS") == "true"
|
|
117
|
+
reproducer = _Reproducer(
|
|
118
|
+
signers=allowed_signers_text(allowed_signers, base_ref),
|
|
119
|
+
landed=landing_times(base_ref or "HEAD"),
|
|
120
|
+
tolerance=Tolerance(atol=atol, rtol=rtol),
|
|
121
|
+
scan_corpora=scan_corpora(corpora),
|
|
122
|
+
corpus_given=bool(corpora),
|
|
123
|
+
allowed_models=allowed_models,
|
|
124
|
+
device=device,
|
|
125
|
+
include_signed=include_signed or sample is not None,
|
|
126
|
+
measurements_only=sample is not None,
|
|
127
|
+
errata=load_errata(wiki_dir),
|
|
128
|
+
)
|
|
129
|
+
results, unreached = _check_all(reproducer, paths, in_actions, time_budget)
|
|
130
|
+
failed = sum(bool(problems) for _, problems, _ in results)
|
|
131
|
+
summary = f"{len(results) - failed}/{len(results)} record(s) reproduced."
|
|
132
|
+
if unreached:
|
|
133
|
+
summary += f" {len(unreached)} not reached in the {time_budget:g}-minute time budget."
|
|
134
|
+
click.echo(summary)
|
|
135
|
+
if in_actions and os.environ.get("GITHUB_STEP_SUMMARY"):
|
|
136
|
+
_step_summary(os.environ["GITHUB_STEP_SUMMARY"], summary, results, unreached)
|
|
137
|
+
if failed:
|
|
138
|
+
raise SystemExit(1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_all(reproducer, paths, in_actions: bool, time_budget: float | None):
|
|
142
|
+
"""Check each record in turn. Return the results, and the records not started.
|
|
143
|
+
|
|
144
|
+
No record starts once ``time_budget`` minutes have passed.
|
|
145
|
+
"""
|
|
146
|
+
import time
|
|
147
|
+
|
|
148
|
+
start = time.monotonic()
|
|
149
|
+
results: list[tuple[Path, list[str], list[str]]] = []
|
|
150
|
+
for n, path in enumerate(paths):
|
|
151
|
+
if time_budget is not None and time.monotonic() - start >= time_budget * 60:
|
|
152
|
+
for later in paths[n:]:
|
|
153
|
+
click.echo(f"NOT REACHED: {later}")
|
|
154
|
+
return results, paths[n:]
|
|
155
|
+
status, problems, warnings = reproducer.check(path)
|
|
156
|
+
results.append((path, problems, warnings))
|
|
157
|
+
_report(path, status, problems, warnings, in_actions)
|
|
158
|
+
return results, []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _chosen_records(records, base_ref, sample, sample_date, wiki_dir) -> list[Path]:
|
|
162
|
+
"""Return the named records, then the branch's changes, then the day's sample."""
|
|
163
|
+
from datetime import UTC, datetime
|
|
164
|
+
|
|
165
|
+
from openinterp.checks import changed_records
|
|
166
|
+
from openinterp.reproduce import rotating_sample
|
|
167
|
+
|
|
168
|
+
paths = list(records)
|
|
169
|
+
if base_ref:
|
|
170
|
+
paths += [p for p in changed_records(wiki_dir, base_ref) if p not in paths]
|
|
171
|
+
if sample:
|
|
172
|
+
day = (sample_date or datetime.now(UTC)).date()
|
|
173
|
+
registry = sorted(experiments_dir(wiki_dir).rglob("*.json"))
|
|
174
|
+
paths += [p for p in rotating_sample(registry, sample, day) if p not in paths]
|
|
175
|
+
return paths
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class _Reproducer:
|
|
180
|
+
"""Checks one record at a time, loading each pinned model once."""
|
|
181
|
+
|
|
182
|
+
signers: object
|
|
183
|
+
landed: dict # record path -> when its signature landed (signing.landing_times)
|
|
184
|
+
tolerance: object
|
|
185
|
+
scan_corpora: dict[str, list[str]] # documents by the SHA-256 of their corpus file
|
|
186
|
+
allowed_models: tuple[str, ...]
|
|
187
|
+
device: str
|
|
188
|
+
corpus_given: bool = False # --corpus given: warn for records it doesn't cover
|
|
189
|
+
include_signed: bool = False
|
|
190
|
+
measurements_only: bool = False # for records from another protocol version
|
|
191
|
+
errata: list = field(default_factory=list) # wiki/errata.jsonl (core/errata.py)
|
|
192
|
+
runners: dict[tuple, object] = field(default_factory=dict)
|
|
193
|
+
|
|
194
|
+
def check(self, path: Path) -> tuple[str, list[str], list[str]]:
|
|
195
|
+
"""(status, mismatches, warnings) for the record at ``path``."""
|
|
196
|
+
from pydantic import ValidationError
|
|
197
|
+
|
|
198
|
+
from openinterp.core.schemas import AuditRecord
|
|
199
|
+
from openinterp.reproduce import reproduce_record
|
|
200
|
+
from openinterp.signing import SignatureError, SignatureOutsideWindow, verify_record
|
|
201
|
+
|
|
202
|
+
notes = []
|
|
203
|
+
try:
|
|
204
|
+
signer = verify_record(path, self.signers, self.landed.get(path.resolve()))
|
|
205
|
+
except SignatureOutsideWindow as exc:
|
|
206
|
+
signer, notes = None, [f"{exc}, so it is reproduced"]
|
|
207
|
+
except SignatureError as exc:
|
|
208
|
+
return "FAILED", [str(exc)], []
|
|
209
|
+
if signer and not self.include_signed:
|
|
210
|
+
return f"SIGNED by {signer} (reproduction skipped)", [], []
|
|
211
|
+
try:
|
|
212
|
+
record = AuditRecord.model_validate_json(path.read_text(encoding="utf-8"))
|
|
213
|
+
except ValidationError:
|
|
214
|
+
return "FAILED", ["invalid experiment record"], []
|
|
215
|
+
record, aside = self.corrected(record, notes)
|
|
216
|
+
if aside:
|
|
217
|
+
return f"SET ASIDE by erratum {aside} (reproduction skipped)", [], notes
|
|
218
|
+
reason = self.refusal(record)
|
|
219
|
+
if reason:
|
|
220
|
+
return "FAILED", [reason], notes
|
|
221
|
+
runner = self.runner(record)
|
|
222
|
+
report = reproduce_record(
|
|
223
|
+
record,
|
|
224
|
+
runner,
|
|
225
|
+
self.tolerance,
|
|
226
|
+
path,
|
|
227
|
+
self.scan_corpus(record, path, notes),
|
|
228
|
+
measurements_only=self.measurements_only,
|
|
229
|
+
)
|
|
230
|
+
status = "FAILED" if report.mismatches else "REPRODUCED"
|
|
231
|
+
return status, report.mismatches, notes + report.warnings
|
|
232
|
+
|
|
233
|
+
def scan_corpus(self, record, path: Path, notes: list[str]):
|
|
234
|
+
"""(documents, SHA-256) of the corpus that ``record`` scanned, if it is known here."""
|
|
235
|
+
prov = record.submission.provenance
|
|
236
|
+
if prov is None or prov.corpus_sample is None:
|
|
237
|
+
return None
|
|
238
|
+
if prov.corpus_sha256 in self.scan_corpora:
|
|
239
|
+
return self.scan_corpora[prov.corpus_sha256], prov.corpus_sha256
|
|
240
|
+
if self.corpus_given:
|
|
241
|
+
notes.append(
|
|
242
|
+
"no corpus given is the scan's, so its sample was not checked. "
|
|
243
|
+
+ _rebuild_hint(record, path)
|
|
244
|
+
)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def corrected(self, record, notes: list[str]):
|
|
248
|
+
"""``record`` with its errata's edits, and the id of an erratum that sets it aside.
|
|
249
|
+
|
|
250
|
+
A harness fix can change what a record reproduces to. Its erratum's edits give the
|
|
251
|
+
corrected values, so those are the values to check.
|
|
252
|
+
"""
|
|
253
|
+
from openinterp.core.errata import apply_errata, covering
|
|
254
|
+
|
|
255
|
+
found = covering(self.errata, record)
|
|
256
|
+
aside = next((x.id for x in found if x.set_aside), None)
|
|
257
|
+
if aside or not found:
|
|
258
|
+
return record, aside
|
|
259
|
+
notes.append(f"checked with the edits of erratum {', '.join(x.id for x in found)}")
|
|
260
|
+
return apply_errata(record, found)[0], None
|
|
261
|
+
|
|
262
|
+
def refusal(self, record) -> str | None:
|
|
263
|
+
"""Why ``record`` can't be reproduced here, or ``None``."""
|
|
264
|
+
from openinterp.reproduce import check_protocol
|
|
265
|
+
|
|
266
|
+
model, allowed = record.submission.identifier.model_name, self.allowed_models
|
|
267
|
+
reason = None if self.measurements_only else check_protocol(record)
|
|
268
|
+
if allowed and model not in allowed:
|
|
269
|
+
reason = f"model {model} is not on the allow-list ({', '.join(allowed)})"
|
|
270
|
+
elif allowed and not _preset_dictionary(record):
|
|
271
|
+
reason = "its dictionary is not a preset's (openinterp/harness/dictionaries.py)"
|
|
272
|
+
return reason
|
|
273
|
+
|
|
274
|
+
def runner(self, record):
|
|
275
|
+
from openinterp.cli import _load_runner
|
|
276
|
+
from openinterp.half_precision import reproduction_dtype
|
|
277
|
+
from openinterp.harness.units import attn_implementation
|
|
278
|
+
from openinterp.reproduce import pinned_environment
|
|
279
|
+
|
|
280
|
+
model = record.submission.identifier.model_name
|
|
281
|
+
revision, pinned = pinned_environment(record)
|
|
282
|
+
dtype = reproduction_dtype(pinned)
|
|
283
|
+
attn = attn_implementation(record.submission.identifier)
|
|
284
|
+
key = (model, revision, dtype, attn)
|
|
285
|
+
if key not in self.runners:
|
|
286
|
+
self.runners[key] = _load_runner(model, self.device, dtype, revision, attn)
|
|
287
|
+
return self.runners[key]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _report(path: Path, status: str, problems, warnings, in_actions: bool) -> None:
|
|
291
|
+
"""One record's result, as GitHub Actions annotations when running there."""
|
|
292
|
+
click.echo(f"{status}: {path}")
|
|
293
|
+
for message in problems:
|
|
294
|
+
click.echo(f"::error file={path}::{message}" if in_actions else f" mismatch: {message}")
|
|
295
|
+
for message in warnings:
|
|
296
|
+
click.echo(f"::warning file={path}::{message}" if in_actions else f" warning: {message}")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _step_summary(path: str, summary: str, results, unreached=()) -> None:
|
|
300
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
301
|
+
fh.write(f"### Reproduction\n\n{summary}\n\n")
|
|
302
|
+
for record, problems, _ in results:
|
|
303
|
+
fh.write(f"- {'❌' if problems else '✅'} `{record}`\n")
|
|
304
|
+
fh.writelines(f" - {message}\n" for message in problems)
|
|
305
|
+
fh.writelines(f"- ⏭️ `{record}` (not reached)\n" for record in unreached)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _preset_dictionary(record) -> bool:
|
|
309
|
+
"""Check that a feature record's dictionary is on the allow-list.
|
|
310
|
+
|
|
311
|
+
A feature record pins the weights of its dictionary's preset. So the allow-list also
|
|
312
|
+
limits which dictionaries are downloaded. Other records have none to check.
|
|
313
|
+
"""
|
|
314
|
+
from openinterp.harness.dictionaries import DICTIONARIES
|
|
315
|
+
|
|
316
|
+
ident, profile = record.submission.identifier, record.submission.dictionary_profile
|
|
317
|
+
if ident.kind != "dictionary_feature" or profile is None:
|
|
318
|
+
return True
|
|
319
|
+
spec = DICTIONARIES.get(ident.dictionary)
|
|
320
|
+
return spec is not None and (spec.repo, spec.revision) == (profile.repo, profile.revision)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@cli.command()
|
|
324
|
+
@click.argument(
|
|
325
|
+
"records", nargs=-1, required=True, type=click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
326
|
+
)
|
|
327
|
+
@click.option(
|
|
328
|
+
"--key",
|
|
329
|
+
default=Path("~/.ssh/openinterp_signing"),
|
|
330
|
+
show_default=True,
|
|
331
|
+
envvar="OPENINTERP_SIGNING_KEY",
|
|
332
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
333
|
+
help="Private SSH key to sign with (or $OPENINTERP_SIGNING_KEY); its public key must be "
|
|
334
|
+
"in --allowed-signers. Use a key kept for signing, not your login key.",
|
|
335
|
+
)
|
|
336
|
+
@click.option(
|
|
337
|
+
"--allowed-signers",
|
|
338
|
+
default=Path(".github/allowed_signers"),
|
|
339
|
+
show_default=True,
|
|
340
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
341
|
+
)
|
|
342
|
+
def sign(records, key, allowed_signers):
|
|
343
|
+
"""Vouch for experiment records with your SSH key, so CI skips reproducing them.
|
|
344
|
+
|
|
345
|
+
Writes <id>.json.sig next to each record; the record itself is not changed. The
|
|
346
|
+
verdict still comes from the review protocol: a signature only says you vouch that
|
|
347
|
+
the record is what it claims to be. Commit the .sig files with the records.
|
|
348
|
+
"""
|
|
349
|
+
from openinterp.signing import (
|
|
350
|
+
SignatureError,
|
|
351
|
+
SignatureOutsideWindow,
|
|
352
|
+
allowed_signers_text,
|
|
353
|
+
sign_record,
|
|
354
|
+
verify_record,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
signers = allowed_signers_text(allowed_signers)
|
|
358
|
+
for path in records:
|
|
359
|
+
sig = sign_record(path, key.expanduser())
|
|
360
|
+
try:
|
|
361
|
+
signer = verify_record(path, signers)
|
|
362
|
+
except SignatureOutsideWindow as exc:
|
|
363
|
+
sig.unlink()
|
|
364
|
+
click.echo(f"Did not sign {path}: {exc}.", err=True)
|
|
365
|
+
raise SystemExit(1) from None
|
|
366
|
+
except SignatureError:
|
|
367
|
+
click.echo(
|
|
368
|
+
f"Wrote {sig}, but {key}.pub is not in {allowed_signers}, so CI will reject "
|
|
369
|
+
"it. Add it with:\n"
|
|
370
|
+
f' echo "<your-name> namespaces=\\"openinterp-record\\" $(cat {key}.pub)" '
|
|
371
|
+
f">> {allowed_signers}",
|
|
372
|
+
err=True,
|
|
373
|
+
)
|
|
374
|
+
raise SystemExit(1) from None
|
|
375
|
+
click.echo(f"Signed {path} as {signer}: {sig}")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@cli.command()
|
|
379
|
+
@click.argument("records", nargs=-1, type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
380
|
+
@click.option(
|
|
381
|
+
"--ref",
|
|
382
|
+
default="HEAD",
|
|
383
|
+
show_default=True,
|
|
384
|
+
help="Check each signature at the time it landed on this ref's first-parent history.",
|
|
385
|
+
)
|
|
386
|
+
@click.option(
|
|
387
|
+
"--allowed-signers",
|
|
388
|
+
default=Path(".github/allowed_signers"),
|
|
389
|
+
show_default=True,
|
|
390
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
391
|
+
)
|
|
392
|
+
@click.option("--paths-only", is_flag=True, help="Print only the paths, one per line.")
|
|
393
|
+
@wiki_dir_option()
|
|
394
|
+
def signatures(records, ref, allowed_signers, paths_only, wiki_dir):
|
|
395
|
+
"""List signed records whose signature CI must not trust (default: every record).
|
|
396
|
+
|
|
397
|
+
Each signature is checked at the time it landed on --ref, against the validity
|
|
398
|
+
window of its key in --allowed-signers. After you revoke a key with valid-before
|
|
399
|
+
(SECURITY.md), use it to find the records signed in its exposure window. Then
|
|
400
|
+
reproduce them: open-interpretability reproduce --include-signed \\
|
|
401
|
+
$(open-interpretability signatures --paths-only).
|
|
402
|
+
"""
|
|
403
|
+
from openinterp.signing import (
|
|
404
|
+
SignatureError,
|
|
405
|
+
allowed_signers_text,
|
|
406
|
+
landing_times,
|
|
407
|
+
signature_path,
|
|
408
|
+
verify_record,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
paths = list(records) or sorted(experiments_dir(wiki_dir).rglob("*.json"))
|
|
412
|
+
paths = [p for p in paths if signature_path(p).exists()]
|
|
413
|
+
signers, landed = allowed_signers_text(allowed_signers), landing_times(ref)
|
|
414
|
+
untrusted = 0
|
|
415
|
+
for path in paths:
|
|
416
|
+
at = landed.get(path.resolve())
|
|
417
|
+
try:
|
|
418
|
+
verify_record(path, signers, at)
|
|
419
|
+
except SignatureError as exc:
|
|
420
|
+
untrusted += 1
|
|
421
|
+
when = "not landed" if at is None else f"landed {at:%Y-%m-%d %H:%M}Z"
|
|
422
|
+
click.echo(path if paths_only else f"{path} ({when}): {exc}")
|
|
423
|
+
if not paths_only:
|
|
424
|
+
click.echo(f"{untrusted} of {len(paths)} signed record(s) not trusted.")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def scan_corpora(paths) -> dict[str, list[str]]:
|
|
428
|
+
"""Documents by the SHA-256 of their corpus file: the built-in corpus and ``paths``."""
|
|
429
|
+
from openinterp.cli_evidence import read_corpus
|
|
430
|
+
|
|
431
|
+
corpora = {}
|
|
432
|
+
for path in (None, *paths):
|
|
433
|
+
texts, _, sha, _ = read_corpus(path)
|
|
434
|
+
corpora[sha] = texts
|
|
435
|
+
return corpora
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _rebuild_hint(record, path: Path) -> str:
|
|
439
|
+
"""How to get the corpus that ``record`` scanned, from its source or the committed table."""
|
|
440
|
+
from openinterp.corpus_rebuild import scanned_source
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
scanned_source(record.submission)
|
|
444
|
+
except ValueError as error:
|
|
445
|
+
return f"Its corpus can't be rebuilt: {error}."
|
|
446
|
+
return f"Rebuild it with `open-interpretability corpus --same-as {path} --out <file>`."
|