omnilane 0.42.4 → 0.42.6
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 +97 -1
- package/README.ja.md +48 -0
- package/README.ko.md +48 -0
- package/README.md +79 -0
- package/README.zh-CN.md +69 -0
- package/README.zh-TW.md +69 -0
- package/VERSION +1 -1
- package/docs/release-notes-0.42.5.md +99 -0
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/scripts/doctor.sh +26 -0
- package/scripts/lib/aa_policy.py +38 -2
- package/scripts/lib/build_overlay.py +214 -0
- package/scripts/lib/overlay_health.py +97 -0
- package/scripts/lib/probe.py +408 -0
- package/skills/omnilane/SKILL.md +83 -1
- package/docs/release-notes-0.42.4.md +0 -47
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Request-selector probe harness for the omnilane AA transport overlay.
|
|
3
|
+
|
|
4
|
+
Runs one CLI invocation, captures raw stdout/stderr to files, and writes a
|
|
5
|
+
descriptor with the original command/stream fields plus a vendor-specific
|
|
6
|
+
verdict, its reason, the observed model, and the tier of evidence that model
|
|
7
|
+
rests on.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import glob
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
SWEEP_ID = os.environ.get("OMNILANE_TRANSPORT_SWEEP_ID", "overlay-reprobe-20260909")
|
|
20
|
+
DEFAULT_ROOT = Path.home() / ".omnilane" / "transport-evidence" / SWEEP_ID
|
|
21
|
+
|
|
22
|
+
# Ordered strongest first. The tier is read off the evidence a run produced, not
|
|
23
|
+
# off the vendor: a CLI that starts reporting a billed model earns the higher
|
|
24
|
+
# tier with no change here.
|
|
25
|
+
TIER_BILLED = "billed-model" # the provider named the model it charged for
|
|
26
|
+
TIER_ECHO = "client-echo" # the CLI recorded the model it asked for
|
|
27
|
+
TIER_SELECTOR = "selector-only" # the CLI accepted the selector and said no more
|
|
28
|
+
EVIDENCE_TIERS = (TIER_BILLED, TIER_ECHO, TIER_SELECTOR)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def codex_failure(stdout_text: str) -> str:
|
|
32
|
+
"""The last thing codex's event stream said went wrong, if anything.
|
|
33
|
+
|
|
34
|
+
Later events supersede earlier ones: a retry notice is progress, the message
|
|
35
|
+
on `turn.failed` is the outcome.
|
|
36
|
+
"""
|
|
37
|
+
latest = ""
|
|
38
|
+
for line in stdout_text.splitlines():
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if not line.startswith("{"):
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
event = json.loads(line)
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
continue
|
|
46
|
+
if not isinstance(event, dict):
|
|
47
|
+
continue
|
|
48
|
+
if event.get("type") == "turn.failed":
|
|
49
|
+
message = (event.get("error") or {}).get("message")
|
|
50
|
+
elif event.get("type") == "error":
|
|
51
|
+
message = event.get("message")
|
|
52
|
+
else:
|
|
53
|
+
continue
|
|
54
|
+
if isinstance(message, str) and message:
|
|
55
|
+
latest = message
|
|
56
|
+
return f"; codex-event: {latest[:200]}" if latest else ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _requested_model(command: list) -> str | None:
|
|
60
|
+
requested = None
|
|
61
|
+
for index, argument in enumerate(command):
|
|
62
|
+
if not isinstance(argument, str):
|
|
63
|
+
continue
|
|
64
|
+
if argument in ("--model", "-m") and index + 1 < len(command):
|
|
65
|
+
requested = command[index + 1]
|
|
66
|
+
elif argument.startswith("--model="):
|
|
67
|
+
requested = argument.split("=", 1)[1]
|
|
68
|
+
return requested
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def record_values(record_text: str) -> list[str]:
|
|
72
|
+
"""Comparable values from a client record digest: `key<TAB>value` lines.
|
|
73
|
+
|
|
74
|
+
`#` lines carry provenance and display labels that must never be compared —
|
|
75
|
+
agy's backend label is "Gemini 3.8 Flash (Low)", not a model identifier.
|
|
76
|
+
"""
|
|
77
|
+
values = []
|
|
78
|
+
for line in (record_text or "").splitlines():
|
|
79
|
+
if line.startswith("#") or "\t" not in line:
|
|
80
|
+
continue
|
|
81
|
+
value = line.split("\t", 1)[1].strip()
|
|
82
|
+
if value:
|
|
83
|
+
values.append(value)
|
|
84
|
+
return values
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _client_record(
|
|
88
|
+
evidence_json: dict,
|
|
89
|
+
record_text: str,
|
|
90
|
+
passed_reason: str,
|
|
91
|
+
) -> tuple[str, str, str | None, str]:
|
|
92
|
+
"""Judge a CLI's own on-disk record of the request it sent."""
|
|
93
|
+
values = record_values(record_text)
|
|
94
|
+
if not values:
|
|
95
|
+
return "pass", f"{passed_reason}; no-client-record", None, TIER_SELECTOR
|
|
96
|
+
observed_model = ", ".join(sorted(set(values)))
|
|
97
|
+
requested_model = _requested_model(evidence_json.get("command", []))
|
|
98
|
+
if not requested_model:
|
|
99
|
+
return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
|
|
100
|
+
if requested_model not in values:
|
|
101
|
+
return "fail", "client-record-mismatch", observed_model, TIER_SELECTOR
|
|
102
|
+
return "pass", f"{passed_reason}-and-client-record-matched", observed_model, TIER_ECHO
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def verdict(
|
|
106
|
+
evidence_json: dict,
|
|
107
|
+
stdout_text: str,
|
|
108
|
+
stderr_text: str,
|
|
109
|
+
vendor: str,
|
|
110
|
+
expected_token: str | None,
|
|
111
|
+
extra: dict | None = None,
|
|
112
|
+
) -> tuple[str, str, str | None, str]:
|
|
113
|
+
"""Judge raw evidence without reading files, running commands or mutating it.
|
|
114
|
+
|
|
115
|
+
`extra` carries text the caller already gathered from disk, so this stays a
|
|
116
|
+
pure function that can re-judge an old sweep offline.
|
|
117
|
+
"""
|
|
118
|
+
extra = extra or {}
|
|
119
|
+
if evidence_json.get("timed_out"):
|
|
120
|
+
return "fail", "timeout", None, TIER_SELECTOR
|
|
121
|
+
if not expected_token:
|
|
122
|
+
return "fail", "missing-expected-token", None, TIER_SELECTOR
|
|
123
|
+
|
|
124
|
+
exit_code = evidence_json.get("exit_code")
|
|
125
|
+
if vendor == "claude":
|
|
126
|
+
try:
|
|
127
|
+
response = json.loads(stdout_text)
|
|
128
|
+
except (json.JSONDecodeError, TypeError):
|
|
129
|
+
return "fail", "invalid-json", None, TIER_SELECTOR
|
|
130
|
+
if not isinstance(response, dict):
|
|
131
|
+
return "fail", "invalid-json-result", None, TIER_SELECTOR
|
|
132
|
+
usage = response.get("modelUsage")
|
|
133
|
+
models = sorted(usage) if isinstance(usage, dict) else []
|
|
134
|
+
observed_model = ", ".join(models) or None
|
|
135
|
+
result = response.get("result", "")
|
|
136
|
+
if not isinstance(result, str):
|
|
137
|
+
result = str(result)
|
|
138
|
+
if response.get("is_error"):
|
|
139
|
+
lower_result = result.lower()
|
|
140
|
+
if "limit" in lower_result or "quota" in lower_result:
|
|
141
|
+
reason = "quota-exhausted"
|
|
142
|
+
elif "api error" in lower_result:
|
|
143
|
+
reason = "api-error"
|
|
144
|
+
else:
|
|
145
|
+
reason = "result-error"
|
|
146
|
+
return "fail", f"{reason}: {result[:120]}", observed_model, TIER_SELECTOR
|
|
147
|
+
if not models:
|
|
148
|
+
return "fail", "missing-model-usage", None, TIER_SELECTOR
|
|
149
|
+
requested_model = _requested_model(evidence_json.get("command", []))
|
|
150
|
+
if not requested_model:
|
|
151
|
+
return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
|
|
152
|
+
if models != [requested_model]:
|
|
153
|
+
return "fail", "model-mismatch", observed_model, TIER_SELECTOR
|
|
154
|
+
if "unknown --effort" in stderr_text.lower():
|
|
155
|
+
return "fail", "effort-silently-defaulted", observed_model, TIER_SELECTOR
|
|
156
|
+
if exit_code != 0:
|
|
157
|
+
return "fail", f"exit-code: {exit_code}", observed_model, TIER_SELECTOR
|
|
158
|
+
if expected_token not in result:
|
|
159
|
+
return "fail", "missing-expected-token", observed_model, TIER_SELECTOR
|
|
160
|
+
return "pass", "expected-token-and-model-matched", observed_model, TIER_BILLED
|
|
161
|
+
|
|
162
|
+
if vendor == "grok":
|
|
163
|
+
if exit_code != 0:
|
|
164
|
+
return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None, TIER_SELECTOR
|
|
165
|
+
if stderr_text:
|
|
166
|
+
return "fail", f"unexpected-stderr: {stderr_text[:120]}", None, TIER_SELECTOR
|
|
167
|
+
if expected_token not in stdout_text:
|
|
168
|
+
return "fail", "missing-expected-token", None, TIER_SELECTOR
|
|
169
|
+
try:
|
|
170
|
+
response = json.loads(stdout_text)
|
|
171
|
+
except (json.JSONDecodeError, TypeError):
|
|
172
|
+
response = None
|
|
173
|
+
usage = response.get("modelUsage") if isinstance(response, dict) else None
|
|
174
|
+
models = sorted(usage) if isinstance(usage, dict) else []
|
|
175
|
+
if not models:
|
|
176
|
+
return "pass", "expected-token-and-clean-stderr; no-billed-model", None, TIER_SELECTOR
|
|
177
|
+
observed_model = ", ".join(models)
|
|
178
|
+
requested_model = _requested_model(evidence_json.get("command", []))
|
|
179
|
+
if not requested_model:
|
|
180
|
+
return "fail", "missing-requested-model", observed_model, TIER_SELECTOR
|
|
181
|
+
# Grok bills `grok-4.6` as `grok-4.6-build`. Accept that one suffix and
|
|
182
|
+
# nothing else: a prefix test would let `grok-4.6-anything` pass.
|
|
183
|
+
if models not in ([requested_model], [f"{requested_model}-build"]):
|
|
184
|
+
return "fail", "model-mismatch", observed_model, TIER_SELECTOR
|
|
185
|
+
return "pass", "expected-token-and-billed-model-matched", observed_model, TIER_BILLED
|
|
186
|
+
|
|
187
|
+
if vendor == "agy":
|
|
188
|
+
if exit_code != 0:
|
|
189
|
+
return "fail", f"exit-code: {exit_code}: {stderr_text[:120]}", None, TIER_SELECTOR
|
|
190
|
+
if stderr_text:
|
|
191
|
+
return "fail", f"unexpected-stderr: {stderr_text[:120]}", None, TIER_SELECTOR
|
|
192
|
+
if expected_token not in stdout_text:
|
|
193
|
+
return "fail", "missing-expected-token", None, TIER_SELECTOR
|
|
194
|
+
return _client_record(evidence_json, extra.get("cli_log", ""),
|
|
195
|
+
"expected-token-and-clean-stderr")
|
|
196
|
+
|
|
197
|
+
if vendor == "codex":
|
|
198
|
+
diagnostics = [line[:120] for line in stderr_text.splitlines()
|
|
199
|
+
if "error" in line.lower() or "warning" in line.lower()]
|
|
200
|
+
review = "; stderr-review: " + " | ".join(diagnostics) if diagnostics else ""
|
|
201
|
+
# Under `--json` the refusal that ended the run is an stdout event, not
|
|
202
|
+
# a stderr line, so a failure would otherwise be recorded as a bare exit
|
|
203
|
+
# code and leave unproven[] saying nothing a reader can act on.
|
|
204
|
+
why = codex_failure(stdout_text)
|
|
205
|
+
if exit_code != 0:
|
|
206
|
+
return "fail", f"exit-code: {exit_code}{why}{review}", None, TIER_SELECTOR
|
|
207
|
+
if expected_token not in stdout_text:
|
|
208
|
+
return "fail", f"missing-expected-token{why}{review}", None, TIER_SELECTOR
|
|
209
|
+
result, reason, observed, tier = _client_record(
|
|
210
|
+
evidence_json, extra.get("rollout", ""), "expected-token-matched")
|
|
211
|
+
return result, f"{reason}{review}", observed, tier
|
|
212
|
+
|
|
213
|
+
return "fail", f"unsupported-vendor: {vendor}", None, TIER_SELECTOR
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _sha256(path: Path) -> str:
|
|
217
|
+
import hashlib
|
|
218
|
+
|
|
219
|
+
digest = hashlib.sha256()
|
|
220
|
+
with open(path, "rb") as stream:
|
|
221
|
+
for block in iter(lambda: stream.read(1 << 20), b""):
|
|
222
|
+
digest.update(block)
|
|
223
|
+
return digest.hexdigest()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _walk_models(node, prefix: str, found: dict) -> None:
|
|
227
|
+
if isinstance(node, dict):
|
|
228
|
+
for key, value in node.items():
|
|
229
|
+
here = f"{prefix}.{key}" if prefix else key
|
|
230
|
+
if key == "model" and isinstance(value, str):
|
|
231
|
+
found.setdefault(here, value)
|
|
232
|
+
_walk_models(value, here, found)
|
|
233
|
+
elif isinstance(node, list):
|
|
234
|
+
for item in node:
|
|
235
|
+
_walk_models(item, f"{prefix}[]", found)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def codex_record(stdout_text: str, sessions_dir: Path | None = None) -> str:
|
|
239
|
+
"""Digest the rollout codex persisted for the thread this run started.
|
|
240
|
+
|
|
241
|
+
Needs `codex exec --json` for the thread id and no `--ephemeral`, which
|
|
242
|
+
would suppress the rollout this reads.
|
|
243
|
+
"""
|
|
244
|
+
thread_id = None
|
|
245
|
+
for line in stdout_text.splitlines():
|
|
246
|
+
line = line.strip()
|
|
247
|
+
if not line.startswith("{"):
|
|
248
|
+
continue
|
|
249
|
+
try:
|
|
250
|
+
event = json.loads(line)
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
continue
|
|
253
|
+
if isinstance(event, dict) and isinstance(event.get("thread_id"), str):
|
|
254
|
+
thread_id = event["thread_id"]
|
|
255
|
+
break
|
|
256
|
+
if not thread_id:
|
|
257
|
+
return ""
|
|
258
|
+
root = sessions_dir or Path.home() / ".codex" / "sessions"
|
|
259
|
+
matches = sorted(glob.glob(str(root / "**" / f"rollout-*{thread_id}.jsonl"), recursive=True))
|
|
260
|
+
if not matches:
|
|
261
|
+
return ""
|
|
262
|
+
path = Path(matches[0])
|
|
263
|
+
found: dict[str, str] = {}
|
|
264
|
+
for line in path.read_text(errors="replace").splitlines():
|
|
265
|
+
line = line.strip()
|
|
266
|
+
if not line.startswith("{"):
|
|
267
|
+
continue
|
|
268
|
+
try:
|
|
269
|
+
event = json.loads(line)
|
|
270
|
+
except json.JSONDecodeError:
|
|
271
|
+
continue
|
|
272
|
+
_walk_models(event, "", found)
|
|
273
|
+
lines = [f"# thread_id\t{thread_id}", f"# source\t{path}", f"# sha256\t{_sha256(path)}"]
|
|
274
|
+
lines += [f"{key}\t{value}" for key, value in sorted(found.items())]
|
|
275
|
+
return "\n".join(lines) + "\n"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
AGY_MODEL = re.compile(r"Resolving model (\S+)")
|
|
279
|
+
AGY_LABEL = re.compile(r'selected model override to backend: label="([^"]+)"')
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def agy_record(app_root: Path, since: float) -> str:
|
|
283
|
+
"""Digest the model agy resolved in the logs this run wrote under app_root.
|
|
284
|
+
|
|
285
|
+
`since` rejects logs from an earlier run, so a reused app root cannot lend
|
|
286
|
+
its model string to a later probe.
|
|
287
|
+
"""
|
|
288
|
+
app_root = Path(app_root).expanduser()
|
|
289
|
+
if not app_root.is_dir():
|
|
290
|
+
return ""
|
|
291
|
+
lines: list[str] = []
|
|
292
|
+
values: dict[str, str] = {}
|
|
293
|
+
for path in sorted(app_root.rglob("cli*.log")):
|
|
294
|
+
if not path.is_file() or path.stat().st_mtime < since - 2:
|
|
295
|
+
continue
|
|
296
|
+
text = path.read_text(errors="replace")
|
|
297
|
+
models = AGY_MODEL.findall(text)
|
|
298
|
+
if not models:
|
|
299
|
+
continue
|
|
300
|
+
lines += [f"# source\t{path}", f"# sha256\t{_sha256(path)}"]
|
|
301
|
+
lines += [f"# label\t{label}" for label in dict.fromkeys(AGY_LABEL.findall(text))]
|
|
302
|
+
for model in models:
|
|
303
|
+
values.setdefault(f"{path.name}:resolved-model:{model}", model)
|
|
304
|
+
if not values:
|
|
305
|
+
return ""
|
|
306
|
+
lines += [f"{key}\t{value}" for key, value in sorted(values.items())]
|
|
307
|
+
return "\n".join(lines) + "\n"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def probe(
|
|
311
|
+
name: str,
|
|
312
|
+
argv: list[str],
|
|
313
|
+
timeout: int = 180,
|
|
314
|
+
cwd: Path | None = None,
|
|
315
|
+
root: Path | None = None,
|
|
316
|
+
*,
|
|
317
|
+
vendor: str | None = None,
|
|
318
|
+
expected_token: str | None = None,
|
|
319
|
+
app_root: Path | None = None,
|
|
320
|
+
) -> dict:
|
|
321
|
+
if not expected_token:
|
|
322
|
+
raise ValueError("expected_token is required before running a probe")
|
|
323
|
+
if not argv:
|
|
324
|
+
raise ValueError("command is required")
|
|
325
|
+
vendor = vendor or Path(argv[0]).name
|
|
326
|
+
if vendor not in ("claude", "grok", "agy", "codex"):
|
|
327
|
+
raise ValueError(f"unsupported vendor: {vendor}; pass vendor explicitly")
|
|
328
|
+
root = (root or DEFAULT_ROOT).expanduser()
|
|
329
|
+
evidence = root / "evidence"
|
|
330
|
+
work = root / "work"
|
|
331
|
+
evidence.mkdir(parents=True, exist_ok=True)
|
|
332
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
333
|
+
out_path = evidence / f"{name}.stdout"
|
|
334
|
+
err_path = evidence / f"{name}.stderr"
|
|
335
|
+
started = time.time()
|
|
336
|
+
timed_out = False
|
|
337
|
+
env = dict(os.environ)
|
|
338
|
+
# The runners drop the API key so the subscription OAuth path is used.
|
|
339
|
+
env.pop("XAI_API_KEY", None)
|
|
340
|
+
with open(out_path, "wb") as out, open(err_path, "wb") as err:
|
|
341
|
+
proc = subprocess.Popen(argv, stdout=out, stderr=err, stdin=subprocess.DEVNULL,
|
|
342
|
+
cwd=str(cwd or work), env=env)
|
|
343
|
+
try:
|
|
344
|
+
rc = proc.wait(timeout=timeout)
|
|
345
|
+
except subprocess.TimeoutExpired:
|
|
346
|
+
timed_out = True
|
|
347
|
+
proc.kill()
|
|
348
|
+
rc = proc.wait()
|
|
349
|
+
record = {
|
|
350
|
+
"command": argv,
|
|
351
|
+
"cwd": str(cwd or work),
|
|
352
|
+
"exit_code": rc,
|
|
353
|
+
"timed_out": timed_out,
|
|
354
|
+
"elapsed_seconds": round(time.time() - started, 3),
|
|
355
|
+
"probed_at": datetime.fromtimestamp(started, timezone.utc).isoformat(),
|
|
356
|
+
"stdout": str(out_path),
|
|
357
|
+
"stderr": str(err_path),
|
|
358
|
+
}
|
|
359
|
+
stdout_text = out_path.read_text(errors="replace")
|
|
360
|
+
extra = {}
|
|
361
|
+
if vendor == "codex":
|
|
362
|
+
extra["rollout"] = codex_record(stdout_text)
|
|
363
|
+
elif vendor == "agy" and app_root is not None:
|
|
364
|
+
extra["cli_log"] = agy_record(app_root, started)
|
|
365
|
+
for key, text in extra.items():
|
|
366
|
+
if not text:
|
|
367
|
+
continue
|
|
368
|
+
# Stored so the sweep hashes it and verdict() can re-judge it offline.
|
|
369
|
+
record_path = evidence / f"{name}.{key}"
|
|
370
|
+
record_path.write_text(text)
|
|
371
|
+
record[key] = str(record_path)
|
|
372
|
+
record["verdict"], record["verdict_reason"], record["observed_model"], \
|
|
373
|
+
record["evidence_tier"] = verdict(
|
|
374
|
+
record, stdout_text, err_path.read_text(errors="replace"),
|
|
375
|
+
vendor, expected_token, extra,
|
|
376
|
+
)
|
|
377
|
+
(evidence / f"{name}.json").write_text(json.dumps(record, indent=2) + "\n")
|
|
378
|
+
return record
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def read(
|
|
382
|
+
name: str,
|
|
383
|
+
stream: str = "stdout",
|
|
384
|
+
limit: int = 4000,
|
|
385
|
+
root: Path | None = None,
|
|
386
|
+
) -> str:
|
|
387
|
+
path = (root or DEFAULT_ROOT).expanduser() / "evidence" / f"{name}.{stream}"
|
|
388
|
+
if not path.exists():
|
|
389
|
+
return ""
|
|
390
|
+
return path.read_text(errors="replace")[:limit]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
395
|
+
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
|
396
|
+
parser.add_argument("--expect", required=True, help="expected response token")
|
|
397
|
+
parser.add_argument("--vendor", choices=("claude", "grok", "agy", "codex"),
|
|
398
|
+
help="defaults to the command executable's basename")
|
|
399
|
+
parser.add_argument("--app-root", type=Path,
|
|
400
|
+
help="agy app data root for this run, to read back its cli log")
|
|
401
|
+
parser.add_argument("name")
|
|
402
|
+
parser.add_argument("command", nargs=argparse.REMAINDER)
|
|
403
|
+
args = parser.parse_args()
|
|
404
|
+
if not args.command:
|
|
405
|
+
parser.error("command is required")
|
|
406
|
+
rec = probe(args.name, args.command, root=args.root, app_root=args.app_root,
|
|
407
|
+
vendor=args.vendor, expected_token=args.expect)
|
|
408
|
+
print(json.dumps(rec, ensure_ascii=False))
|
package/skills/omnilane/SKILL.md
CHANGED
|
@@ -371,12 +371,94 @@ closes lanes and pushes the question back onto the operator. Never raise the
|
|
|
371
371
|
declared effort to unblock a refused target, and never assert
|
|
372
372
|
`--operator-asserted-human` on your own behalf.
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
Three refusal codes mean different things and need different fixes.
|
|
375
375
|
`missing-caller-context` means you passed no file — write one.
|
|
376
376
|
`runtime-mapping-unverified` means the file is fine but the *target* has no proven
|
|
377
377
|
host-local request selector; that is fixed by a `--transport-overlay` entry backed
|
|
378
378
|
by real evidence, never by editing the frozen registry (its sha256 is pinned in
|
|
379
379
|
`scripts/lib/aa_policy.py`, so any edit fails the whole gate closed).
|
|
380
|
+
`invalid-policy-input` with "transport contract evidence changed" is neither: the
|
|
381
|
+
overlay itself will not load, so nothing about your caller or your target is wrong.
|
|
382
|
+
Run `omnilane doctor` first — its `transport-overlay` check names the offending
|
|
383
|
+
file and the vendor it belongs to. Do not go hunting by hand.
|
|
384
|
+
|
|
385
|
+
Upgrading a vendor CLI is the usual cause. The overlay pins the sha256 of each
|
|
386
|
+
vendor's executable and runner script, so a new release invalidates that vendor's
|
|
387
|
+
selector evidence. Evidence entries carry a `vendor` tag: a tagged entry that
|
|
388
|
+
drifts marks only its own vendor stale, and the other three keep dispatching.
|
|
389
|
+
Untagged evidence — `probe-manifest.json`, and any overlay built before the tags
|
|
390
|
+
existed — still fails the whole gate closed, which is what an unpatched host
|
|
391
|
+
looks like. Codex and Claude resolve through version directories
|
|
392
|
+
(`releases/0.153.4-…`, `versions/2.1.266`), so their upgrades remove the anchored
|
|
393
|
+
file rather than change its digest; both are treated as staleness, not corruption.
|
|
394
|
+
|
|
395
|
+
Do not expect these upgrades to be operator actions. agy and grok update
|
|
396
|
+
themselves in the background when invoked — agy's own `cli.log` records
|
|
397
|
+
`auto_updater.go: Spawned background update process`, and both binaries changed
|
|
398
|
+
under a probing session on 2026-09-10, minutes after their first call. Overlay
|
|
399
|
+
drift is therefore a routine consequence of using a vendor, not an occasional
|
|
400
|
+
maintenance event, which is why per-vendor degradation matters more than it
|
|
401
|
+
looks. It also means any test asserting a fixed number of verified live
|
|
402
|
+
mappings will go red on its own schedule.
|
|
403
|
+
|
|
404
|
+
Because of that, `build_overlay.py` anchors the executable `shutil.which` resolves
|
|
405
|
+
rather than a version written into the script. A pinned path drifts out of use
|
|
406
|
+
silently: before 0.42.6 the overlay hashed claude `2.1.263` while every dispatch
|
|
407
|
+
ran `2.1.266`, so eleven mappings were "verified" against a binary that had not
|
|
408
|
+
run for a day.
|
|
409
|
+
|
|
410
|
+
Re-signing is a probe, a rebuild, and an install, in that order. Back up
|
|
411
|
+
`~/.omnilane/transport-contracts.local.json` first; restoring it is the rollback.
|
|
412
|
+
`scripts/lib/probe.py --expect TOKEN [--vendor V] NAME COMMAND…` invokes the CLI
|
|
413
|
+
directly through `subprocess`, so it works while the gate is refusing everything —
|
|
414
|
+
this is what breaks the deadlock. `scripts/provider-probe.sh` goes through
|
|
415
|
+
`dispatch.sh` and therefore through the gate, so it is useless in this state.
|
|
416
|
+
Then `scripts/lib/build_overlay.py` rebuilds, and you copy the result over the
|
|
417
|
+
live overlay. Verify with a real dispatch on a lane belonging to the vendor you
|
|
418
|
+
re-probed; loading the registry in Python is not the runtime surface.
|
|
419
|
+
|
|
420
|
+
Keep the sweep where its default `--root` puts it,
|
|
421
|
+
`~/.omnilane/transport-evidence/<sweep-id>/`. The rebuilt overlay anchors
|
|
422
|
+
`probe-manifest.json` by absolute path as untagged evidence, so a sweep parked
|
|
423
|
+
inside a repository is one `git clean -fdx` away from taking every vendor down
|
|
424
|
+
at once — the same global refusal a re-signing session is usually trying to end.
|
|
425
|
+
|
|
426
|
+
Every mapping carries an `evidence_tier` saying how strongly its probe pinned the
|
|
427
|
+
responder. `billed-model` means the provider named the model it charged for —
|
|
428
|
+
Claude's `modelUsage`, grok's under `--output-format json`. `client-echo` means
|
|
429
|
+
the CLI wrote down the model it asked for — codex's session rollout, agy's
|
|
430
|
+
`cli.log` resolver line. `selector-only` means the CLI accepted the selector and
|
|
431
|
+
said nothing more. Put plainly: `client-echo` is the CLI's copy of your order,
|
|
432
|
+
`billed-model` is the provider's receipt. Neither certifies upstream identity,
|
|
433
|
+
but only one of them was written by the party that answered.
|
|
434
|
+
|
|
435
|
+
The tier is reported, never enforced. Dispatch still turns on `runtime_verified`
|
|
436
|
+
alone, so a mapping that drops to `selector-only` keeps working and simply shows
|
|
437
|
+
up in doctor as worth re-probing. Do not add a tier check to the gate: that would
|
|
438
|
+
rebuild the failure 0.42.5 removed, where evidence quality could refuse a lane
|
|
439
|
+
that runs. The tier is read off the evidence a run produced rather than assigned
|
|
440
|
+
per vendor, so a sweep predating 0.42.6 re-judges as `selector-only` and a CLI
|
|
441
|
+
that starts reporting a billed model is promoted with no code change.
|
|
442
|
+
|
|
443
|
+
Two probe details follow from this. Codex needs `exec --json` (the thread id that
|
|
444
|
+
locates the rollout) and must *not* use `--ephemeral`, which suppresses the very
|
|
445
|
+
rollout the tier reads. agy needs its own app data directory, prepared exactly
|
|
446
|
+
the way `run-gemini.sh` does it — `prepare-agy-mode.py --mode advise` returns a
|
|
447
|
+
path relative to `~/.gemini` that is passed as `--app_data_dir=`; the environment
|
|
448
|
+
variables that look like they would do this are ignored.
|
|
449
|
+
|
|
450
|
+
Never sign a probe you did not read. `probe.py` records a `verdict` because exit
|
|
451
|
+
status alone is not evidence: the Claude CLI answers a quota refusal with a JSON
|
|
452
|
+
body carrying `is_error`, and it accepts an unknown `--effort` by silently using
|
|
453
|
+
the default, returning exit 0, the right `modelUsage`, and the expected token
|
|
454
|
+
with only a stderr warning to show for it. Effort is half of a scored identity,
|
|
455
|
+
so that path would certify a mapping at the wrong tier. Configurations whose
|
|
456
|
+
probes failed are recorded in the overlay's `unproven[]` and surfaced by doctor
|
|
457
|
+
instead of vanishing — six Fable rows sat unusable for two days in September
|
|
458
|
+
2026 because a 429 quota refusal left no trace anywhere. A refused probe is not
|
|
459
|
+
always transient: re-probing those six two days later returned the same 429, so
|
|
460
|
+
an `unproven[]` entry can mean the account, not the moment. Read the reason
|
|
461
|
+
before assuming a retry will clear it.
|
|
380
462
|
|
|
381
463
|
A `--transport-overlay /absolute/overlay.json` may prove a small set of host-local
|
|
382
464
|
request selectors using exact identities and hashed local contract evidence. It does
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Omnilane 0.42.4
|
|
2
|
-
|
|
3
|
-
This patch fixes the user-facing quickstart. It changes no routing, scoring, gate,
|
|
4
|
-
runner, or CLI behaviour.
|
|
5
|
-
|
|
6
|
-
## Why
|
|
7
|
-
|
|
8
|
-
0.42.3 documented `--caller-context` inside the dispatch skill, which is what a
|
|
9
|
-
model driving omnilane reads. It left the READMEs' 60-second start untouched —
|
|
10
|
-
and that is the path a new install actually takes. A user who ran
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm i -g omnilane
|
|
14
|
-
omnilane route hardest-coding "fix the flaky auth token refresh"
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
was refused with `missing-caller-context`, and no README section explained the
|
|
18
|
-
flag that resolves it. The gate was working as designed; the documentation simply
|
|
19
|
-
never told a first-time user how to satisfy it.
|
|
20
|
-
|
|
21
|
-
## Changes
|
|
22
|
-
|
|
23
|
-
- The 60-second start in all five READMEs asserts the human operator once with
|
|
24
|
-
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` before the first `omnilane route`.
|
|
25
|
-
- A note after the quickstart explains why a dispatch must say who is asking:
|
|
26
|
-
a human at a terminal asserts it with that variable or `--operator-asserted-human`
|
|
27
|
-
per call; a model driving omnilane cannot assert it for itself and passes
|
|
28
|
-
`--caller-context FILE` with its exact vendor, model, and effort instead; with
|
|
29
|
-
neither, the dispatch is refused before any job is created.
|
|
30
|
-
- The `dispatch.sh` synopsis in the command reference now shows
|
|
31
|
-
`[--caller-context FILE | --operator-asserted-human]`.
|
|
32
|
-
|
|
33
|
-
## Verification boundary
|
|
34
|
-
|
|
35
|
-
`--operator-asserted-human` is cooperative operator metadata. It is not automatic
|
|
36
|
-
model detection and not OS authentication, and this release does not change that.
|
|
37
|
-
A model caller still must not assert it on its own behalf.
|
|
38
|
-
|
|
39
|
-
The frozen AA registry and its approved SHA are unchanged. Coverage remains 78
|
|
40
|
-
scored targets, one scored reference-only entry, and 10 unknown configurations.
|
|
41
|
-
|
|
42
|
-
## Upgrade
|
|
43
|
-
|
|
44
|
-
After npm publication, run `npm i -g omnilane@0.42.4`. An existing repo-symlink
|
|
45
|
-
installation can update its checkout and verify `omnilane --version` without
|
|
46
|
-
rerunning installation. GitHub release and npm publication remain separate
|
|
47
|
-
verification surfaces from Linux CI.
|