omnilane 0.42.8 → 0.44.0
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +189 -1
- package/README.ja.md +180 -20
- package/README.ko.md +179 -20
- package/README.md +282 -61
- package/README.zh-CN.md +178 -19
- package/README.zh-TW.md +168 -41
- package/VERSION +1 -1
- package/bin/omnilane +29 -0
- package/completions/_omnilane +1 -1
- package/completions/omnilane.bash +1 -1
- package/completions/omnilane.fish +2 -0
- package/docs/native-executor.md +59 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/dispatch.sh +54 -4
- package/scripts/doctor.sh +7 -1
- package/scripts/lib/aa_lanes.py +135 -0
- package/scripts/lib/aa_policy.py +154 -1
- package/scripts/lib/build_overlay.py +45 -18
- package/scripts/lib/caller_identity.py +78 -14
- package/scripts/lib/cli_provenance.py +82 -0
- package/scripts/lib/native.py +107 -2
- package/scripts/lib/native_context.py +145 -0
- package/scripts/lib/overlay_health.py +24 -2
- package/scripts/lib/probe.py +2 -0
- package/scripts/lib/probe_sweep.py +176 -0
- package/scripts/lib/resign.py +475 -0
- package/scripts/release-audit.sh +31 -0
- package/skills/omnilane/SKILL.md +375 -464
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Re-sign this host's AA transport overlay after a vendor CLI changed.
|
|
3
|
+
|
|
4
|
+
Vendor CLIs update themselves, so the overlay's executable pins go stale as
|
|
5
|
+
routine traffic, and every lane of that vendor is then refused. This finds the
|
|
6
|
+
drift and, when it is safe to, repairs it without an operator:
|
|
7
|
+
|
|
8
|
+
detect drift -> check the signer -> canary -> re-probe into a staging root
|
|
9
|
+
-> build and load the staging overlay -> replace the live one atomically
|
|
10
|
+
-> one real dispatch per re-probed vendor -> restore the old one on failure
|
|
11
|
+
|
|
12
|
+
A changed executable is re-probed unattended only when it still carries the
|
|
13
|
+
signer the live overlay recorded and still sits in the same install location.
|
|
14
|
+
Anything else stops at a notification; `--approve VENDOR` is the operator saying
|
|
15
|
+
they looked. `--trust-adhoc VENDOR` is the operator saying an adhoc signature in
|
|
16
|
+
that install location is their own local step, so those updates count as
|
|
17
|
+
same-signer.
|
|
18
|
+
|
|
19
|
+
Exit codes: 0 nothing to do, or re-signed and verified; 10 drift found (--check);
|
|
20
|
+
20 drift needs an operator; 30 attempted and rolled back; 2 not configured.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import contextlib
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
36
|
+
import aa_policy # noqa: E402
|
|
37
|
+
import build_overlay # noqa: E402
|
|
38
|
+
import cli_provenance # noqa: E402
|
|
39
|
+
import probe_sweep # noqa: E402
|
|
40
|
+
|
|
41
|
+
EXIT_OK, EXIT_DRIFT, EXIT_OPERATOR, EXIT_ROLLED_BACK, EXIT_UNCONFIGURED = 0, 10, 20, 30, 2
|
|
42
|
+
VENDORS = probe_sweep.VENDORS
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sha256(path: Path) -> str:
|
|
46
|
+
return build_overlay.sha256(path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def current_anchors() -> dict[str, dict[str, Path]]:
|
|
50
|
+
"""What the runners would execute right now, per vendor."""
|
|
51
|
+
anchors = {}
|
|
52
|
+
for vendor in VENDORS:
|
|
53
|
+
found = shutil.which(build_overlay.CLI_NAMES[vendor])
|
|
54
|
+
anchors[vendor] = {
|
|
55
|
+
"cli": Path(found).resolve() if found else None,
|
|
56
|
+
"runner": build_overlay.REPO / "scripts/runners" / build_overlay.RUNNERS[vendor],
|
|
57
|
+
}
|
|
58
|
+
return anchors
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def detect(overlay: dict, anchors: dict[str, dict[str, Path]]) -> dict[str, dict]:
|
|
62
|
+
"""Per vendor: what no longer matches the live overlay, and the signer it recorded."""
|
|
63
|
+
report = {}
|
|
64
|
+
for vendor in VENDORS:
|
|
65
|
+
recorded = [entry for entry in overlay.get("evidence", []) if entry.get("vendor") == vendor]
|
|
66
|
+
runner_name = build_overlay.RUNNERS[vendor]
|
|
67
|
+
recorded_cli = next((e for e in recorded if Path(e["path"]).name != runner_name), None)
|
|
68
|
+
recorded_runner = next((e for e in recorded if Path(e["path"]).name == runner_name), None)
|
|
69
|
+
reasons = []
|
|
70
|
+
cli, runner = anchors[vendor]["cli"], anchors[vendor]["runner"]
|
|
71
|
+
if cli is None:
|
|
72
|
+
reasons.append(f"{build_overlay.CLI_NAMES[vendor]} is not on PATH")
|
|
73
|
+
elif recorded_cli is None:
|
|
74
|
+
reasons.append("the overlay pins no executable for this vendor")
|
|
75
|
+
elif str(cli) != recorded_cli["path"]:
|
|
76
|
+
# doctor hashes the recorded path, which an update leaves behind untouched.
|
|
77
|
+
reasons.append(f"runs {cli}, overlay pins {recorded_cli['path']}")
|
|
78
|
+
elif sha256(cli) != recorded_cli["sha256"]:
|
|
79
|
+
reasons.append(f"{cli} changed")
|
|
80
|
+
if recorded_runner is None or not runner.is_file():
|
|
81
|
+
reasons.append(f"{runner_name} is not pinned")
|
|
82
|
+
elif sha256(runner) != recorded_runner["sha256"]:
|
|
83
|
+
reasons.append(f"{runner_name} changed")
|
|
84
|
+
report[vendor] = {
|
|
85
|
+
"drifted": bool(reasons),
|
|
86
|
+
"reasons": reasons,
|
|
87
|
+
"cli": str(cli) if cli else None,
|
|
88
|
+
"cli_changed": any(runner_name not in reason for reason in reasons),
|
|
89
|
+
"recorded_cli_path": recorded_cli["path"] if recorded_cli else None,
|
|
90
|
+
"recorded_codesign": recorded_signer(recorded_cli),
|
|
91
|
+
}
|
|
92
|
+
return report
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def recorded_signer(entry: dict | None) -> dict | None:
|
|
96
|
+
"""The signer the overlay recorded for a CLI, with any waiver the operator attached."""
|
|
97
|
+
if not entry:
|
|
98
|
+
return None
|
|
99
|
+
signer = dict(entry.get("codesign") or {})
|
|
100
|
+
if entry.get("operator_trust"):
|
|
101
|
+
signer["operator_trust"] = entry["operator_trust"]
|
|
102
|
+
return signer or None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def gate(vendor_report: dict, approved: bool) -> tuple[bool, str]:
|
|
106
|
+
if not vendor_report["cli"]:
|
|
107
|
+
return False, "the CLI is not installed"
|
|
108
|
+
if not vendor_report["cli_changed"]:
|
|
109
|
+
return True, "only omnilane's own runner script changed"
|
|
110
|
+
current = cli_provenance.facts(vendor_report["cli"])
|
|
111
|
+
vendor_report["codesign"] = current
|
|
112
|
+
allowed, reason = cli_provenance.verdict(
|
|
113
|
+
vendor_report["recorded_codesign"], vendor_report["recorded_cli_path"],
|
|
114
|
+
current, vendor_report["cli"])
|
|
115
|
+
if not allowed and approved:
|
|
116
|
+
return True, f"approved by the operator ({reason})"
|
|
117
|
+
return allowed, reason
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def canary(cli: str, runner=subprocess.run) -> tuple[bool, str]:
|
|
121
|
+
try:
|
|
122
|
+
result = runner([cli, "--version"], capture_output=True, text=True, timeout=30,
|
|
123
|
+
stdin=subprocess.DEVNULL)
|
|
124
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
125
|
+
return False, f"--version did not run: {error.__class__.__name__}"
|
|
126
|
+
text = (result.stdout or result.stderr).strip().splitlines()
|
|
127
|
+
if result.returncode != 0 or not text:
|
|
128
|
+
return False, f"--version exited {result.returncode}"
|
|
129
|
+
return True, text[0][:80]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def previous_root(overlay: dict) -> Path | None:
|
|
133
|
+
for entry in overlay.get("evidence", []):
|
|
134
|
+
if "vendor" not in entry and Path(entry["path"]).name == "probe-manifest.json":
|
|
135
|
+
return Path(entry["path"]).parent
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def verified(overlay_path: Path) -> dict[str, int]:
|
|
140
|
+
"""Load an overlay the way dispatch does and count what it verifies per vendor."""
|
|
141
|
+
saved = {key: os.environ.get(key) for key in
|
|
142
|
+
("OMNILANE_AA_TRANSPORT_OVERLAY", "OMNILANE_AA_OVERLAY_SHA256")}
|
|
143
|
+
os.environ["OMNILANE_AA_TRANSPORT_OVERLAY"] = str(overlay_path)
|
|
144
|
+
os.environ.pop("OMNILANE_AA_OVERLAY_SHA256", None)
|
|
145
|
+
try:
|
|
146
|
+
registry, _ = aa_policy.load_registry(build_overlay.REPO / "config/aa-model-policy.json")
|
|
147
|
+
finally:
|
|
148
|
+
for key, value in saved.items():
|
|
149
|
+
if value is None:
|
|
150
|
+
os.environ.pop(key, None)
|
|
151
|
+
else:
|
|
152
|
+
os.environ[key] = value
|
|
153
|
+
counts = {vendor: 0 for vendor in VENDORS}
|
|
154
|
+
stale = set(registry.get("_stale_transport_vendors", []))
|
|
155
|
+
for row in registry["scored_configs"]:
|
|
156
|
+
if row["vendor"] in counts and row["vendor"] not in stale \
|
|
157
|
+
and row["transport_mapping"].get("runtime_verified") is True:
|
|
158
|
+
counts[row["vendor"]] += 1
|
|
159
|
+
return counts
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def smoke(vendor: str, overlay: dict, overlay_path: Path, timeout: int = 300,
|
|
163
|
+
runner=subprocess.run) -> tuple[bool, str]:
|
|
164
|
+
"""One real advise dispatch on the vendor's cheapest verified selector.
|
|
165
|
+
|
|
166
|
+
The human assertion skips the ceiling, not the transport: the job still goes
|
|
167
|
+
through dispatch.sh, the runner and the CLI. The mapping gate itself was
|
|
168
|
+
already exercised by verified().
|
|
169
|
+
"""
|
|
170
|
+
rows = [(build_overlay.ROWS[m["config_id"]], m) for m in overlay["mappings"]
|
|
171
|
+
if build_overlay.ROWS[m["config_id"]]["vendor"] == vendor
|
|
172
|
+
and (m["runtime_effort"] or m["selector_type"] == "model_id_encoded_effort"
|
|
173
|
+
or vendor == "claude")]
|
|
174
|
+
if not rows:
|
|
175
|
+
return False, "no verified mapping to dispatch"
|
|
176
|
+
row, mapping = min(rows, key=lambda pair: (pair[0]["score"], pair[0]["id"]))
|
|
177
|
+
token = f"OMNILANE_RESIGN_SMOKE_{vendor.upper()}"
|
|
178
|
+
command = ["bash", str(build_overlay.REPO / "scripts/dispatch.sh"), "--operator-asserted-human",
|
|
179
|
+
"--background", "--single-shot", "--timeout", str(timeout), "--vendor", vendor,
|
|
180
|
+
"--model", mapping["runtime_model"]]
|
|
181
|
+
if mapping["selector_type"] != "model_id_encoded_effort" and mapping["runtime_effort"]:
|
|
182
|
+
command += ["--effort", mapping["runtime_effort"]]
|
|
183
|
+
command += ["consult", f"Reply with exactly the text {token} and nothing else. Do not use tools."]
|
|
184
|
+
env = dict(os.environ, OMNILANE_AA_TRANSPORT_OVERLAY=str(overlay_path))
|
|
185
|
+
env.pop("OMNILANE_AA_OVERLAY_SHA256", None)
|
|
186
|
+
try:
|
|
187
|
+
started = runner(command, capture_output=True, text=True, timeout=120, env=env,
|
|
188
|
+
stdin=subprocess.DEVNULL)
|
|
189
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
190
|
+
return False, f"dispatch did not start: {error.__class__.__name__}"
|
|
191
|
+
job = started.stdout.strip().splitlines()[-1] if started.stdout.strip() else ""
|
|
192
|
+
if started.returncode != 0 or not job:
|
|
193
|
+
return False, f"dispatch exited {started.returncode}: {started.stderr.strip()[-200:]}"
|
|
194
|
+
home = Path(os.environ.get("OMNILANE_HOME") or Path.home() / ".omnilane")
|
|
195
|
+
directory = home / "jobs" / job
|
|
196
|
+
deadline = time.time() + timeout + 60
|
|
197
|
+
while time.time() < deadline and not (directory / "exit").exists():
|
|
198
|
+
time.sleep(3)
|
|
199
|
+
try:
|
|
200
|
+
code = (directory / "exit").read_text().strip()
|
|
201
|
+
answer = (directory / "out.txt").read_text(errors="replace")
|
|
202
|
+
except OSError:
|
|
203
|
+
return False, f"job {job} did not finish"
|
|
204
|
+
if code != "0" or token not in answer:
|
|
205
|
+
return False, f"job {job} exited {code} without the token"
|
|
206
|
+
return True, f"job {job} on {row['id']} answered"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def record_signers(live: Path, overlay: dict, report: dict, wanted: list[str], log) -> int:
|
|
210
|
+
"""Operator action: adopt the signer of every executable the overlay already pins.
|
|
211
|
+
|
|
212
|
+
An overlay signed before 0.43.0 recorded no signer, so its first drift would
|
|
213
|
+
always stop for an operator. This is that operator decision made ahead of
|
|
214
|
+
time, and only for an executable whose path and hash still match the pin.
|
|
215
|
+
"""
|
|
216
|
+
changed = []
|
|
217
|
+
for entry in overlay.get("evidence", []):
|
|
218
|
+
vendor = entry.get("vendor")
|
|
219
|
+
if vendor not in wanted or Path(entry["path"]).name == build_overlay.RUNNERS[vendor]:
|
|
220
|
+
continue
|
|
221
|
+
if report[vendor]["cli_changed"]:
|
|
222
|
+
log(f"omnilane: {vendor}: drifted, so its signer is not adopted; re-sign it instead")
|
|
223
|
+
continue
|
|
224
|
+
facts = cli_provenance.facts(entry["path"])
|
|
225
|
+
if entry.get("codesign") != facts:
|
|
226
|
+
entry["codesign"] = facts
|
|
227
|
+
changed.append(f"{vendor}={facts['signer']}")
|
|
228
|
+
if not changed:
|
|
229
|
+
log("omnilane: every pinned executable already has its signer recorded")
|
|
230
|
+
return EXIT_OK
|
|
231
|
+
backup = live.with_name(live.name + ".before-record-signers-" + datetime.now().strftime("%Y%m%d-%H%M%S"))
|
|
232
|
+
shutil.copy2(live, backup)
|
|
233
|
+
aa_policy.atomic_bytes(live, (json.dumps(overlay, indent=2, ensure_ascii=False) + "\n").encode())
|
|
234
|
+
try:
|
|
235
|
+
verified(live)
|
|
236
|
+
except (aa_policy.PolicyError, OSError, ValueError) as error:
|
|
237
|
+
aa_policy.atomic_bytes(live, backup.read_bytes())
|
|
238
|
+
log(f"omnilane: the overlay stopped loading ({error}); restored {backup}")
|
|
239
|
+
return EXIT_ROLLED_BACK
|
|
240
|
+
log(f"omnilane: recorded signers {', '.join(changed)} (backup {backup})")
|
|
241
|
+
return EXIT_OK
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def trust_adhoc(live: Path, overlay: dict, report: dict, wanted: list[str], log) -> int:
|
|
245
|
+
"""Operator action: an adhoc executable of this vendor, in its current install
|
|
246
|
+
location, may be re-probed unattended from now on.
|
|
247
|
+
|
|
248
|
+
The operator is saying a local step re-signs this CLI on every update, so the
|
|
249
|
+
vendor's team will never be on it. The waiver is bound to the install location
|
|
250
|
+
the overlay pins; a new directory or an unsigned executable still stops.
|
|
251
|
+
"""
|
|
252
|
+
changed = []
|
|
253
|
+
for entry in overlay.get("evidence", []):
|
|
254
|
+
vendor = entry.get("vendor")
|
|
255
|
+
if vendor not in wanted or Path(entry["path"]).name == build_overlay.RUNNERS[vendor]:
|
|
256
|
+
continue
|
|
257
|
+
if entry.get("operator_trust") == cli_provenance.TRUST_ADHOC:
|
|
258
|
+
continue
|
|
259
|
+
if not entry.get("codesign"):
|
|
260
|
+
log(f"omnilane: {vendor} has no recorded signer to bind the trust to; "
|
|
261
|
+
"run omnilane resign --record-signers first")
|
|
262
|
+
continue
|
|
263
|
+
entry["operator_trust"] = cli_provenance.TRUST_ADHOC
|
|
264
|
+
entry["operator_trust_recorded_at"] = datetime.now(timezone.utc).isoformat()
|
|
265
|
+
changed.append(f"{vendor} in {cli_provenance.family(entry['path'])}")
|
|
266
|
+
if not changed:
|
|
267
|
+
log("omnilane: nothing to record; that vendor is already trusted adhoc, or the overlay pins no executable for it")
|
|
268
|
+
return EXIT_OK
|
|
269
|
+
backup = live.with_name(live.name + ".before-trust-adhoc-" + datetime.now().strftime("%Y%m%d-%H%M%S"))
|
|
270
|
+
shutil.copy2(live, backup)
|
|
271
|
+
aa_policy.atomic_bytes(live, (json.dumps(overlay, indent=2, ensure_ascii=False) + "\n").encode())
|
|
272
|
+
try:
|
|
273
|
+
verified(live)
|
|
274
|
+
except (aa_policy.PolicyError, OSError, ValueError) as error:
|
|
275
|
+
aa_policy.atomic_bytes(live, backup.read_bytes())
|
|
276
|
+
log(f"omnilane: the overlay stopped loading ({error}); restored {backup}")
|
|
277
|
+
return EXIT_ROLLED_BACK
|
|
278
|
+
log(f"omnilane: trusting adhoc {', '.join(changed)} (backup {backup})")
|
|
279
|
+
return EXIT_OK
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def resign(args, log=print) -> int:
|
|
283
|
+
overlay_env = os.environ.get("OMNILANE_AA_TRANSPORT_OVERLAY")
|
|
284
|
+
if not overlay_env:
|
|
285
|
+
log("omnilane: no transport overlay is configured (OMNILANE_AA_TRANSPORT_OVERLAY); "
|
|
286
|
+
"there is nothing to re-sign. See the README, 'Let your AI assistant drive omnilane', Step 2.")
|
|
287
|
+
return EXIT_UNCONFIGURED
|
|
288
|
+
live = Path(overlay_env).expanduser()
|
|
289
|
+
try:
|
|
290
|
+
overlay = json.loads(live.read_text())
|
|
291
|
+
except (OSError, ValueError) as error:
|
|
292
|
+
log(f"omnilane: cannot read the live overlay {live}: {error}")
|
|
293
|
+
return EXIT_UNCONFIGURED
|
|
294
|
+
report = detect(overlay, current_anchors())
|
|
295
|
+
wanted = args.vendor or list(VENDORS)
|
|
296
|
+
if args.record_signers:
|
|
297
|
+
return record_signers(live, overlay, report, wanted, log)
|
|
298
|
+
if args.trust_adhoc:
|
|
299
|
+
return trust_adhoc(live, overlay, report, args.trust_adhoc, log)
|
|
300
|
+
drifted = [vendor for vendor in wanted if report[vendor]["drifted"]]
|
|
301
|
+
summary = {"host": overlay.get("host"), "checked_at": datetime.now(timezone.utc).isoformat(),
|
|
302
|
+
"live_overlay": str(live), "live_sha256": sha256(live), "vendors": report}
|
|
303
|
+
if not drifted:
|
|
304
|
+
log("omnilane: transport overlay matches what the runners execute; nothing to re-sign")
|
|
305
|
+
if args.json:
|
|
306
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
307
|
+
return EXIT_OK
|
|
308
|
+
for vendor in drifted:
|
|
309
|
+
log(f"omnilane: {vendor} drifted: " + "; ".join(report[vendor]["reasons"]))
|
|
310
|
+
if args.check:
|
|
311
|
+
if args.json:
|
|
312
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
313
|
+
log("omnilane: run `omnilane resign` to re-probe and re-sign")
|
|
314
|
+
return EXIT_DRIFT
|
|
315
|
+
|
|
316
|
+
proceed, held = [], []
|
|
317
|
+
for vendor in drifted:
|
|
318
|
+
allowed, reason = gate(report[vendor], vendor in (args.approve or []))
|
|
319
|
+
report[vendor]["gate"] = {"allowed": allowed, "reason": reason}
|
|
320
|
+
if allowed and report[vendor]["cli"]:
|
|
321
|
+
alive, detail = canary(report[vendor]["cli"])
|
|
322
|
+
report[vendor]["canary"] = {"ok": alive, "detail": detail}
|
|
323
|
+
allowed, reason = (allowed, reason) if alive else (False, f"canary failed: {detail}")
|
|
324
|
+
(proceed if allowed else held).append(vendor)
|
|
325
|
+
log(f"omnilane: {vendor}: {'re-probing' if allowed else 'NOT re-signing'} - {reason}")
|
|
326
|
+
|
|
327
|
+
sweep_id = "resign-" + datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
328
|
+
home = Path(os.environ.get("OMNILANE_HOME") or Path.home() / ".omnilane")
|
|
329
|
+
if (home / "transport-evidence" / sweep_id).exists():
|
|
330
|
+
sweep_id += "-" + os.urandom(2).hex() # two runs in one second
|
|
331
|
+
root = home / "transport-evidence" / sweep_id
|
|
332
|
+
outcome = EXIT_OK
|
|
333
|
+
if proceed:
|
|
334
|
+
source = previous_root(overlay)
|
|
335
|
+
if source is None or not (source / "evidence").is_dir():
|
|
336
|
+
log("omnilane: the live overlay's probe evidence is gone; a full sweep is needed "
|
|
337
|
+
"(scripts/lib/probe_sweep.py --root NEW, then build_overlay.py --root NEW)")
|
|
338
|
+
return EXIT_OPERATOR
|
|
339
|
+
root.mkdir(parents=True)
|
|
340
|
+
shutil.copytree(source / "evidence", root / "evidence")
|
|
341
|
+
shutil.copy2(live, root / "live-overlay.BEFORE.json")
|
|
342
|
+
sweeps = {vendor: probe_sweep.sweep(vendor, root, log=log) for vendor in proceed}
|
|
343
|
+
summary["sweeps"] = sweeps
|
|
344
|
+
unfinished = [vendor for vendor, result in sweeps.items() if result["outcome"] != "done"]
|
|
345
|
+
for vendor in unfinished:
|
|
346
|
+
log(f"omnilane: {vendor}: {sweeps[vendor]['outcome']} - {sweeps[vendor]['detail']}")
|
|
347
|
+
# A selector the live overlay verifies and this sweep could not is more
|
|
348
|
+
# likely a provider having a bad hour than a selector that stopped working.
|
|
349
|
+
# Installing that would trade a stale pin for a smaller overlay.
|
|
350
|
+
was_verified = {mapping["config_id"] for mapping in overlay.get("mappings", [])}
|
|
351
|
+
for vendor, result in sweeps.items():
|
|
352
|
+
lost = sorted(was_verified & set(result["failed"]))
|
|
353
|
+
if lost and vendor not in unfinished and not args.allow_shrink:
|
|
354
|
+
result["regressed"] = lost
|
|
355
|
+
unfinished.append(vendor)
|
|
356
|
+
log(f"omnilane: {vendor}: {len(lost)} selector(s) the live overlay verifies failed "
|
|
357
|
+
f"this time ({', '.join(lost)}); keeping the old pin. Retry later, or pass "
|
|
358
|
+
"--allow-shrink if they are really gone")
|
|
359
|
+
if unfinished:
|
|
360
|
+
held += unfinished
|
|
361
|
+
proceed = [vendor for vendor in proceed if vendor not in unfinished]
|
|
362
|
+
# An unfinished vendor keeps its old evidence so the rebuild stays honest about it.
|
|
363
|
+
for vendor in unfinished:
|
|
364
|
+
for entry in probe_sweep.plan(vendor):
|
|
365
|
+
for old in (source / "evidence").glob(entry["name"] + ".*"):
|
|
366
|
+
shutil.copy2(old, root / "evidence" / old.name)
|
|
367
|
+
if proceed:
|
|
368
|
+
with contextlib.redirect_stdout(sys.stderr): # stdout is reserved for --json
|
|
369
|
+
build_overlay.main(["--root", str(root), "--source",
|
|
370
|
+
f"{overlay.get('host')} / omnilane resign {sweep_id} / "
|
|
371
|
+
f"re-probed: {', '.join(proceed)}"])
|
|
372
|
+
staged_path = root / "transport-contracts.local.json"
|
|
373
|
+
staged = json.loads(staged_path.read_text())
|
|
374
|
+
# Only a re-probed vendor gets a new pin; every other drifted one keeps the
|
|
375
|
+
# pin it had, because its new executable was never probed.
|
|
376
|
+
unprobed = [vendor for vendor in VENDORS if report[vendor]["drifted"] and vendor not in proceed]
|
|
377
|
+
for index, entry in enumerate(staged["evidence"]):
|
|
378
|
+
if entry.get("codesign") is not None:
|
|
379
|
+
# build_overlay anchors every vendor afresh, so a trust recorded on an
|
|
380
|
+
# untouched vendor would vanish with a re-sign of another one.
|
|
381
|
+
old = next((e for e in overlay["evidence"] if e.get("vendor") == entry["vendor"]
|
|
382
|
+
and e.get("operator_trust")), None)
|
|
383
|
+
if old is not None:
|
|
384
|
+
entry["operator_trust"] = old["operator_trust"]
|
|
385
|
+
entry["operator_trust_recorded_at"] = old.get("operator_trust_recorded_at")
|
|
386
|
+
if entry.get("vendor") in unprobed:
|
|
387
|
+
name = Path(entry["path"]).name
|
|
388
|
+
is_runner = name == build_overlay.RUNNERS[entry["vendor"]]
|
|
389
|
+
old = next((e for e in overlay["evidence"] if e.get("vendor") == entry["vendor"]
|
|
390
|
+
and (Path(e["path"]).name == build_overlay.RUNNERS[e["vendor"]]) == is_runner),
|
|
391
|
+
None)
|
|
392
|
+
if old is not None:
|
|
393
|
+
staged["evidence"][index] = old
|
|
394
|
+
staged_path.write_text(json.dumps(staged, indent=2, ensure_ascii=False) + "\n")
|
|
395
|
+
try:
|
|
396
|
+
before, after = verified(live), verified(staged_path)
|
|
397
|
+
except (aa_policy.PolicyError, OSError, ValueError) as error:
|
|
398
|
+
log(f"omnilane: the staged overlay does not load ({error}); live overlay untouched")
|
|
399
|
+
return EXIT_ROLLED_BACK
|
|
400
|
+
summary["verified"] = {"before": before, "after": after}
|
|
401
|
+
empty = [vendor for vendor in proceed if after[vendor] == 0]
|
|
402
|
+
if empty:
|
|
403
|
+
log(f"omnilane: staged overlay verifies nothing for {', '.join(empty)}; "
|
|
404
|
+
"live overlay untouched")
|
|
405
|
+
return EXIT_ROLLED_BACK
|
|
406
|
+
for vendor in proceed:
|
|
407
|
+
if after[vendor] < max(before[vendor], len(sweeps[vendor]["passed"])):
|
|
408
|
+
log(f"omnilane: {vendor}: fewer selectors verified than expected "
|
|
409
|
+
f"({before[vendor]} -> {after[vendor]}); see {root}/evidence")
|
|
410
|
+
backup = live.with_name(live.name + f".before-{sweep_id}")
|
|
411
|
+
shutil.copy2(live, backup)
|
|
412
|
+
aa_policy.atomic_bytes(live, staged_path.read_bytes())
|
|
413
|
+
log(f"omnilane: installed {staged_path} over {live} (backup {backup})")
|
|
414
|
+
failures = []
|
|
415
|
+
if not args.no_smoke:
|
|
416
|
+
for vendor in proceed:
|
|
417
|
+
ok, detail = smoke(vendor, staged, live)
|
|
418
|
+
summary.setdefault("smoke", {})[vendor] = {"ok": ok, "detail": detail}
|
|
419
|
+
log(f"omnilane: {vendor}: smoke {'passed' if ok else 'FAILED'} - {detail}")
|
|
420
|
+
if not ok:
|
|
421
|
+
failures.append(vendor)
|
|
422
|
+
if failures:
|
|
423
|
+
aa_policy.atomic_bytes(live, backup.read_bytes())
|
|
424
|
+
log(f"omnilane: smoke failed for {', '.join(failures)}; restored {backup}")
|
|
425
|
+
outcome = EXIT_ROLLED_BACK
|
|
426
|
+
if held and outcome == EXIT_OK:
|
|
427
|
+
outcome = EXIT_OPERATOR
|
|
428
|
+
for vendor in held:
|
|
429
|
+
sweep = summary.get("sweeps", {}).get(vendor, {})
|
|
430
|
+
if sweep.get("outcome") == "unprobeable":
|
|
431
|
+
# Waiting changes nothing either: the CLI has to be logged in first.
|
|
432
|
+
log(f"omnilane: {vendor} was not re-signed; its old pin stays. {sweep['detail']}: "
|
|
433
|
+
f"omnilane resign --vendor {vendor}")
|
|
434
|
+
elif report[vendor].get("gate", {}).get("allowed"):
|
|
435
|
+
# The signer was fine; the probes were not. Approval would change nothing.
|
|
436
|
+
log(f"omnilane: {vendor} was not re-signed; its old pin stays. Retry later: "
|
|
437
|
+
f"omnilane resign --vendor {vendor}")
|
|
438
|
+
else:
|
|
439
|
+
log(f"omnilane: {vendor} still needs an operator. After checking the executable: "
|
|
440
|
+
f"omnilane resign --vendor {vendor} --approve {vendor}")
|
|
441
|
+
summary.update(re_signed=proceed if outcome != EXIT_ROLLED_BACK else [], held=held,
|
|
442
|
+
exit_code=outcome, sweep_root=str(root) if root.exists() else None)
|
|
443
|
+
if root.exists():
|
|
444
|
+
(root / "resign-report.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2,
|
|
445
|
+
default=str) + "\n")
|
|
446
|
+
if args.json:
|
|
447
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
|
|
448
|
+
return outcome
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def main(argv: list[str] | None = None) -> int:
|
|
452
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0],
|
|
453
|
+
epilog=__doc__.split("Exit codes:")[1].strip())
|
|
454
|
+
parser.add_argument("--check", action="store_true", help="report drift and change nothing")
|
|
455
|
+
parser.add_argument("--vendor", action="append", choices=VENDORS,
|
|
456
|
+
help="limit to this vendor; may repeat")
|
|
457
|
+
parser.add_argument("--approve", action="append", choices=VENDORS,
|
|
458
|
+
help="operator approval to re-probe this vendor despite its signer check")
|
|
459
|
+
parser.add_argument("--trust-adhoc", action="append", choices=VENDORS, metavar="VENDOR",
|
|
460
|
+
help="operator action: this vendor's executable is re-signed adhoc by a local "
|
|
461
|
+
"step, so an adhoc update in the same install location may be re-probed "
|
|
462
|
+
"unattended; may repeat")
|
|
463
|
+
parser.add_argument("--record-signers", action="store_true",
|
|
464
|
+
help="operator action: adopt the signers of the executables already pinned")
|
|
465
|
+
parser.add_argument("--allow-shrink", action="store_true",
|
|
466
|
+
help="install even when a selector the live overlay verifies failed this time")
|
|
467
|
+
parser.add_argument("--no-smoke", action="store_true",
|
|
468
|
+
help="skip the real dispatch after installing (not for unattended use)")
|
|
469
|
+
parser.add_argument("--json", action="store_true", help="print the full report as JSON")
|
|
470
|
+
args = parser.parse_args(argv)
|
|
471
|
+
return resign(args, log=lambda text: print(text, file=sys.stderr))
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
if __name__ == "__main__":
|
|
475
|
+
raise SystemExit(main())
|
package/scripts/release-audit.sh
CHANGED
|
@@ -305,6 +305,37 @@ else
|
|
|
305
305
|
fail changelog-release-link
|
|
306
306
|
fi
|
|
307
307
|
|
|
308
|
+
# A runner script is pinned by this host's transport overlay. Shipping a changed
|
|
309
|
+
# one without re-signing refuses every lane of that vendor on the releasing host
|
|
310
|
+
# (0.42.8 did exactly that), and no offline test notices.
|
|
311
|
+
audit_overlay="${OMNILANE_AA_TRANSPORT_OVERLAY:-}"
|
|
312
|
+
if [[ -z "$audit_overlay" && -f "${OMNILANE_HOME:-$HOME/.omnilane}/local.sh" ]]; then
|
|
313
|
+
audit_overlay="$(
|
|
314
|
+
set +u
|
|
315
|
+
. "${OMNILANE_HOME:-$HOME/.omnilane}/local.sh" 2>/dev/null
|
|
316
|
+
printf '%s' "${OMNILANE_AA_TRANSPORT_OVERLAY:-}"
|
|
317
|
+
)"
|
|
318
|
+
fi
|
|
319
|
+
if [[ -z "$audit_overlay" || ! -r "$audit_overlay" ]] || ! command -v python3 >/dev/null 2>&1; then
|
|
320
|
+
warn runner-pins-unchecked
|
|
321
|
+
else
|
|
322
|
+
runner_drift="$(python3 - "$audit_overlay" "$ROOT" <<'PYTHON' 2>/dev/null || printf 'unreadable'
|
|
323
|
+
import hashlib, json, sys
|
|
324
|
+
from pathlib import Path
|
|
325
|
+
overlay, root = json.load(open(sys.argv[1])), Path(sys.argv[2])
|
|
326
|
+
pinned = {Path(e["path"]).name: e["sha256"] for e in overlay.get("evidence", []) if e.get("vendor")}
|
|
327
|
+
drift = [p.name for p in sorted((root / "scripts/runners").glob("run-*.sh"))
|
|
328
|
+
if p.name in pinned and hashlib.sha256(p.read_bytes()).hexdigest() != pinned[p.name]]
|
|
329
|
+
print(",".join(drift))
|
|
330
|
+
PYTHON
|
|
331
|
+
)"
|
|
332
|
+
if [[ -z "$runner_drift" ]]; then
|
|
333
|
+
pass runner-pins-current
|
|
334
|
+
else
|
|
335
|
+
fail "runner-pins-stale:$runner_drift (run: omnilane resign)"
|
|
336
|
+
fi
|
|
337
|
+
fi
|
|
338
|
+
|
|
308
339
|
required=(
|
|
309
340
|
VERSION LICENSE CHANGELOG.md README.md README.zh-TW.md README.zh-CN.md
|
|
310
341
|
README.ja.md README.ko.md install.sh routing.yaml
|