omnilane 0.42.9 → 0.45.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.
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env python3
2
+ """Probe every selector build_overlay.py knows, one vendor at a time.
3
+
4
+ probe.py runs a single command; this derives the whole set from PROVEN and the
5
+ frozen registry, so a sweep is reproducible on any host without carrying a list
6
+ of commands from the last one. Each vendor is probed independently and reports
7
+ one of three outcomes: done, unprobeable (nobody is logged in, or the session
8
+ cannot reach the keychain), or failed.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import re
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
21
+ import build_overlay # noqa: E402
22
+ import probe as probe_module # noqa: E402
23
+
24
+ VENDORS = ("claude", "codex", "gemini", "grok")
25
+ TOKENS = {"claude": "CLAUDE_SELECTOR_OK", "codex": "CODEX_SELECTOR_OK",
26
+ "gemini": "GEMINI_SELECTOR_OK", "grok": "GROK_SELECTOR_OK"}
27
+ # The keychain-backed CLIs cannot authenticate from a launchd Background session
28
+ # (an ssh login is one); probing there records "not logged in" as if it were a
29
+ # finding about the selector.
30
+ KEYCHAIN_VENDORS = ("claude", "gemini", "grok")
31
+ NOT_AUTHENTICATED = re.compile(
32
+ r"not logged in|not signed in|authentication required|please run /login|grok login"
33
+ r"|failed to authenticate|oauth (session|token) (has )?expired|could not be refreshed"
34
+ r"|invalid api key|unauthorized|\b401\b",
35
+ re.IGNORECASE)
36
+ TRANSIENT = re.compile(r"\b(403|429|500|502|503|529)\b|permission-denied|overloaded|timed? ?out",
37
+ re.IGNORECASE)
38
+
39
+
40
+ def prompt(vendor: str) -> str:
41
+ return f"Reply exactly {TOKENS[vendor]}. Do not use tools or delegate."
42
+
43
+
44
+ def command(vendor: str, model: str, effort: str | None, app_data: str | None = None) -> list[str]:
45
+ text = prompt(vendor)
46
+ if vendor == "claude":
47
+ selector = ["--model", model] + (["--effort", effort] if effort else [])
48
+ return ["claude", "--disable-slash-commands", *selector, "--permission-mode", "dontAsk",
49
+ "--tools", "", "--output-format", "json", "-p", text]
50
+ if vendor == "codex":
51
+ return ["codex", "exec", "--json", "--skip-git-repo-check", "-m", model,
52
+ "-c", f'model_reasoning_effort="{effort or "none"}"',
53
+ "-c", 'approval_policy="never"', "-c", 'sandbox_mode="read-only"', text]
54
+ if vendor == "grok":
55
+ return ["grok", "--no-memory", "--no-subagents", "--no-plan", "--no-alt-screen",
56
+ "--output-format", "json", "--verbatim", "--permission-mode", "dontAsk",
57
+ "--tools", "Read", "--deny", "Bash", "--deny", "Edit", "--disable-web-search",
58
+ "-m", model, "--reasoning-effort", effort or "high", "-p", text]
59
+ if vendor == "gemini":
60
+ return ["agy", f"--app_data_dir={app_data}", "--model", model, "-p", text]
61
+ raise ValueError(f"unsupported vendor: {vendor}")
62
+
63
+
64
+ def plan(vendor: str) -> list[dict]:
65
+ """One entry per evidence file this vendor's PROVEN rows point at."""
66
+ entries: dict[str, dict] = {}
67
+ for config_id, (_, runtime_model, evidence) in sorted(build_overlay.PROVEN.items()):
68
+ row = build_overlay.ROWS[config_id]
69
+ if row["vendor"] != vendor or evidence in entries:
70
+ continue
71
+ entries[evidence] = {"name": evidence, "config_id": config_id, "model": runtime_model,
72
+ "effort": None if vendor == "gemini" else row["effort"]}
73
+ return list(entries.values())
74
+
75
+
76
+ def session_manager(runner=subprocess.run) -> str:
77
+ try:
78
+ return runner(["launchctl", "managername"], capture_output=True, text=True,
79
+ timeout=10).stdout.strip()
80
+ except (OSError, subprocess.SubprocessError):
81
+ return ""
82
+
83
+
84
+ def _text(record: dict) -> str:
85
+ parts = [str(record.get("verdict_reason") or "")]
86
+ for key in ("stdout", "stderr"):
87
+ try:
88
+ parts.append(Path(record[key]).read_text(errors="replace")[:4000])
89
+ except (KeyError, OSError):
90
+ pass
91
+ return "\n".join(parts)
92
+
93
+
94
+ def sweep(vendor: str, root: Path, *, repo: Path = build_overlay.REPO, home: Path | None = None,
95
+ run_probe=probe_module.probe, manager=session_manager, log=print,
96
+ only_missing: bool = False) -> dict:
97
+ """Probe one vendor into root/evidence. Never raises for a probe that merely failed.
98
+
99
+ only_missing probes just the rows root/evidence has no record for: the
100
+ executable is unchanged, so what it already answered still stands.
101
+ """
102
+ home = home or Path.home()
103
+ entries = plan(vendor)
104
+ report = {"vendor": vendor, "outcome": "done", "passed": [], "failed": [], "detail": ""}
105
+ if only_missing:
106
+ entries = [entry for entry in entries
107
+ if not (root / "evidence" / f"{entry['name']}.json").is_file()]
108
+ if not entries:
109
+ report["detail"] = "every row already has probe evidence"
110
+ return report
111
+ if (vendor in KEYCHAIN_VENDORS and sys.platform == "darwin"
112
+ and os.environ.get("OMNILANE_PROBE_ANY_SESSION") != "1"):
113
+ name = manager()
114
+ if name and name != "Aqua":
115
+ report.update(outcome="unprobeable",
116
+ detail=(f"this is a launchd {name} session (an ssh login is one); "
117
+ f"{vendor}'s credentials are in the login keychain, which only an "
118
+ "Aqua (GUI) session can read"))
119
+ return report
120
+ work = root / "work"
121
+ work.mkdir(parents=True, exist_ok=True)
122
+ cli = "agy" if vendor == "gemini" else vendor
123
+ for index, entry in enumerate(entries):
124
+ app_root = None
125
+ app_data = None
126
+ if vendor == "gemini":
127
+ # prepare-agy-mode binds its private root to one workdir, so it is per sweep.
128
+ app_root = home / ".omnilane" / "agy-app" / f"probe-{root.name}-{entry['name']}"
129
+ prepared = subprocess.run(
130
+ [sys.executable, str(repo / "scripts/lib/prepare-agy-mode.py"), "--mode", "advise",
131
+ "--workdir", str(work), "--app-root", str(app_root),
132
+ "--gemini-dir", str(home / ".gemini")],
133
+ capture_output=True, text=True)
134
+ if prepared.returncode != 0:
135
+ report.update(outcome="failed",
136
+ detail=f"prepare-agy-mode failed: {prepared.stderr.strip()[-200:]}")
137
+ return report
138
+ app_data = prepared.stdout.strip()
139
+ argv = command(vendor, entry["model"], entry["effort"], app_data)
140
+ record = {}
141
+ for attempt in (1, 2):
142
+ record = run_probe(entry["name"], argv, root=root, vendor=cli,
143
+ expected_token=TOKENS[vendor], app_root=app_root)
144
+ if record.get("verdict") == "pass" or attempt == 2 or not TRANSIENT.search(_text(record)):
145
+ break
146
+ log(f"{entry['name']}: transient failure, retrying once")
147
+ if record.get("verdict") == "pass":
148
+ report["passed"].append(entry["config_id"])
149
+ log(f"{entry['name']} pass {record.get('evidence_tier')}")
150
+ continue
151
+ if NOT_AUTHENTICATED.search(_text(record)):
152
+ # Not a finding about the selector: keep it out of the evidence.
153
+ for suffix in ("json", "stdout", "stderr", "rollout", "cli_log"):
154
+ (root / "evidence" / f"{entry['name']}.{suffix}").unlink(missing_ok=True)
155
+ report.update(outcome="unprobeable",
156
+ detail=f"{cli} is not logged in on this host; log in, then re-run")
157
+ if index:
158
+ report["detail"] += f" ({index} earlier probe(s) had already answered)"
159
+ return report
160
+ report["failed"].append(entry["config_id"])
161
+ log(f"{entry['name']} fail {str(record.get('verdict_reason'))[:120]}")
162
+ # With only_missing the rows not re-probed still hold, so a new row that
163
+ # fails is a finding about that row, not about the vendor.
164
+ if not report["passed"] and not only_missing:
165
+ report.update(outcome="failed", detail="no selector of this vendor passed")
166
+ return report
167
+
168
+
169
+ def main(argv: list[str] | None = None) -> int:
170
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
171
+ parser.add_argument("--root", type=Path, required=True, help="sweep root to write evidence into")
172
+ parser.add_argument("--vendor", action="append", choices=VENDORS,
173
+ help="vendor to probe; may repeat (default: all four)")
174
+ parser.add_argument("--plan", action="store_true", help="print the commands and probe nothing")
175
+ args = parser.parse_args(argv)
176
+ vendors = args.vendor or list(VENDORS)
177
+ if args.plan:
178
+ for vendor in vendors:
179
+ for entry in plan(vendor):
180
+ print(json.dumps({"name": entry["name"], "argv": command(
181
+ vendor, entry["model"], entry["effort"], "<app-data>")}, ensure_ascii=False))
182
+ return 0
183
+ reports = [sweep(vendor, args.root.expanduser()) for vendor in vendors]
184
+ print(json.dumps(reports, ensure_ascii=False, indent=2))
185
+ return 0 if all(report["outcome"] == "done" for report in reports) else 1
186
+
187
+
188
+ if __name__ == "__main__":
189
+ raise SystemExit(main())