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,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Who signed a vendor CLI, and whether a changed one may be re-signed unattended.
|
|
3
|
+
|
|
4
|
+
The transport overlay pins each vendor's executable by hash. Vendor CLIs update
|
|
5
|
+
themselves, so that pin goes stale as routine traffic. Re-pinning on sight would
|
|
6
|
+
turn the pin into a rubber stamp; this module is what stands in between. A
|
|
7
|
+
changed executable may be re-probed without an operator only when it still
|
|
8
|
+
carries the signer recorded when the overlay was last signed and still lives in
|
|
9
|
+
the same install location. Anything else is a notification, not a re-sign.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
ADHOC = "adhoc"
|
|
18
|
+
UNSIGNED = "unsigned"
|
|
19
|
+
# The operator's statement that this vendor's executable is expected to be adhoc
|
|
20
|
+
# here (a local patch step re-signs it), so an adhoc update in the same install
|
|
21
|
+
# location may be re-probed unattended. Nothing else is waived.
|
|
22
|
+
TRUST_ADHOC = "adhoc-in-install-location"
|
|
23
|
+
# 1.0.25 -> 1.0.30 inside a path is an update; a different directory is not.
|
|
24
|
+
VERSION_SEGMENT = re.compile(r"\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.]+)*")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def facts(path: Path | str, runner=subprocess.run) -> dict:
|
|
28
|
+
"""codesign's view of one executable: the signing team, or why there is none."""
|
|
29
|
+
target = str(path)
|
|
30
|
+
try:
|
|
31
|
+
verified = runner(["codesign", "--verify", "--strict", target],
|
|
32
|
+
capture_output=True, text=True, timeout=60)
|
|
33
|
+
described = runner(["codesign", "-dv", "--verbose=2", target],
|
|
34
|
+
capture_output=True, text=True, timeout=60)
|
|
35
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
36
|
+
return {"signer": UNSIGNED, "identifier": None, "valid": False,
|
|
37
|
+
"detail": f"codesign unavailable: {error.__class__.__name__}"}
|
|
38
|
+
fields = {}
|
|
39
|
+
for line in described.stderr.splitlines():
|
|
40
|
+
key, separator, value = line.partition("=")
|
|
41
|
+
if separator and key not in fields:
|
|
42
|
+
fields[key] = value
|
|
43
|
+
valid = verified.returncode == 0
|
|
44
|
+
team = fields.get("TeamIdentifier")
|
|
45
|
+
if not valid:
|
|
46
|
+
signer = UNSIGNED
|
|
47
|
+
elif fields.get("Signature") == "adhoc" or team in (None, "not set"):
|
|
48
|
+
signer = ADHOC
|
|
49
|
+
else:
|
|
50
|
+
signer = team
|
|
51
|
+
return {"signer": signer, "identifier": fields.get("Identifier"), "valid": valid}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def family(path: Path | str) -> str:
|
|
55
|
+
"""An install location with its version numbers blanked out."""
|
|
56
|
+
return VERSION_SEGMENT.sub("<version>", str(path))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def verdict(recorded: dict | None, recorded_path: str | None, current: dict,
|
|
60
|
+
current_path: Path | str) -> tuple[bool, str]:
|
|
61
|
+
"""May this changed executable be re-probed unattended? And the reason either way."""
|
|
62
|
+
if current["signer"] == UNSIGNED:
|
|
63
|
+
return False, "the executable is unsigned, so nothing ties it to the vendor; an operator has to approve it"
|
|
64
|
+
if current["signer"] == ADHOC:
|
|
65
|
+
if (recorded or {}).get("operator_trust") == TRUST_ADHOC and recorded_path \
|
|
66
|
+
and family(recorded_path) == family(current_path):
|
|
67
|
+
return True, "adhoc, which the operator trusts in this install location"
|
|
68
|
+
if (recorded or {}).get("operator_trust") == TRUST_ADHOC:
|
|
69
|
+
return False, (f"adhoc and installed at {current_path}, outside the location the operator "
|
|
70
|
+
f"trusts, {family(recorded_path)}")
|
|
71
|
+
return False, ("the executable is adhoc, so nothing ties it to the vendor; an operator has to "
|
|
72
|
+
"approve it (or, for a local patch step, trust it here: omnilane resign --trust-adhoc VENDOR)")
|
|
73
|
+
if not recorded or not recorded.get("signer"):
|
|
74
|
+
return False, ("the live overlay recorded no signer for this vendor, so there is nothing "
|
|
75
|
+
"to compare against; an operator approves the first signer")
|
|
76
|
+
if recorded["signer"] != current["signer"]:
|
|
77
|
+
return False, (f"signed by {current['signer']}, but the overlay was signed against "
|
|
78
|
+
f"{recorded['signer']}")
|
|
79
|
+
if recorded_path and family(recorded_path) != family(current_path):
|
|
80
|
+
return False, (f"installed at {current_path}, outside the recorded location "
|
|
81
|
+
f"{family(recorded_path)}")
|
|
82
|
+
return True, f"still signed by {current['signer']} in the recorded install location"
|
package/scripts/lib/native.py
CHANGED
|
@@ -91,7 +91,11 @@ def capability_context(path):
|
|
|
91
91
|
ctx = read_json(path)
|
|
92
92
|
fields(ctx, ("schema_version", "harness", "vendor", "capabilities", "requirements"),
|
|
93
93
|
("current_model", "current_effort", "agent_strategy", "existing_agent",
|
|
94
|
-
"preserve_existing_context", "new_agent_capacity"
|
|
94
|
+
"preserve_existing_context", "new_agent_capacity", "inherits_caller_runtime",
|
|
95
|
+
"caller_identity_verified"))
|
|
96
|
+
for flag in ("inherits_caller_runtime", "caller_identity_verified"):
|
|
97
|
+
if flag in ctx:
|
|
98
|
+
check(type(ctx[flag]) is bool, "invalid " + flag)
|
|
95
99
|
check(type(ctx["schema_version"]) is int and ctx["schema_version"] == 1, "unsupported context version")
|
|
96
100
|
identifier(ctx["harness"], "harness")
|
|
97
101
|
identifier(ctx["vendor"], "vendor")
|
|
@@ -210,6 +214,89 @@ def choose(args, ctx):
|
|
|
210
214
|
return "native", "exact-idle-reuse-match" if strategy == "reuse" else "exact-capability-match", model
|
|
211
215
|
|
|
212
216
|
|
|
217
|
+
def choose_inherited(args, ctx, identity):
|
|
218
|
+
"""A worker spawned with no model override. There is no CLI to fall back to."""
|
|
219
|
+
if ctx is None:
|
|
220
|
+
return None, "no-native-context"
|
|
221
|
+
if ctx.get("inherits_caller_runtime") is not True:
|
|
222
|
+
return None, "host-does-not-assert-runtime-inheritance"
|
|
223
|
+
if args.background or args.session != "auto" or args.thread:
|
|
224
|
+
return None, "cli-session-lifecycle"
|
|
225
|
+
if args.job_timeout or args.idle_timeout:
|
|
226
|
+
return None, "cli-watchdog-required"
|
|
227
|
+
if args.mode == "sysops":
|
|
228
|
+
return None, "sysops-requires-cli"
|
|
229
|
+
if ctx["vendor"] != identity["vendor"]:
|
|
230
|
+
return None, "vendor-mismatch"
|
|
231
|
+
if ctx.get("current_model", identity["model"]) != identity["model"]:
|
|
232
|
+
return None, "current-model-mismatch"
|
|
233
|
+
if ctx.get("agent_strategy", "new") != "new" or "existing_agent" in ctx:
|
|
234
|
+
return None, "inherit-spawns-a-new-agent"
|
|
235
|
+
if ctx.get("new_agent_capacity") == "exhausted":
|
|
236
|
+
return None, "new-agent-capacity-exhausted"
|
|
237
|
+
req = ctx["requirements"]
|
|
238
|
+
if req["isolation"] != "shared-inherited" or req["lifecycle"] != "single-shot":
|
|
239
|
+
return None, "unsupported-isolation-or-lifecycle"
|
|
240
|
+
# Effort is deliberately not matched: inheriting it is the whole point.
|
|
241
|
+
caps = [cap for cap in ctx["capabilities"]
|
|
242
|
+
if cap["model"] == identity["model"] and cap.get("agent_strategy", "new") == "new"
|
|
243
|
+
and args.mode in cap["modes"] and args.workdir in cap["workdirs"]
|
|
244
|
+
and set(req["tools"]) <= set(cap["tools"])
|
|
245
|
+
and "shared-inherited" in cap["isolations"] and "single-shot" in cap["lifecycles"]]
|
|
246
|
+
if not caps:
|
|
247
|
+
return None, "no-capability-row-for-caller-model-mode-and-workdir"
|
|
248
|
+
return "native", "inherited-caller-runtime"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def route_inherited(args, ctx, registry, registry_sha):
|
|
252
|
+
caller = caller_sha = None
|
|
253
|
+
if args.caller_context:
|
|
254
|
+
caller, caller_sha = aa_policy.load_caller(
|
|
255
|
+
args.caller_context, registry, args.expected_caller_sha256)
|
|
256
|
+
host = None
|
|
257
|
+
if caller is None and ctx is not None and "current_model" in ctx:
|
|
258
|
+
host = {"vendor": ctx["vendor"], "model": ctx["current_model"]}
|
|
259
|
+
decision = aa_policy.decide_inherited(
|
|
260
|
+
registry, registry_sha, caller=caller, caller_sha256=caller_sha,
|
|
261
|
+
operator_asserted_human=args.operator_asserted_human, host_asserted=host)
|
|
262
|
+
if not decision["allowed"]:
|
|
263
|
+
print(aa_policy._json_line(decision), end="", file=sys.stderr)
|
|
264
|
+
return 3
|
|
265
|
+
identity = caller["caller"] if caller is not None else host
|
|
266
|
+
executor, reason = choose_inherited(args, ctx, identity)
|
|
267
|
+
if executor is None:
|
|
268
|
+
# An inherited worker exists only inside the harness; the CLI is not a fallback for it.
|
|
269
|
+
decision.update(allowed=False, code="native-inherit-unavailable",
|
|
270
|
+
message="this harness cannot take an inherited native worker: " + reason,
|
|
271
|
+
failed_gate="native-capability", reason=reason, child_context=None,
|
|
272
|
+
next_command="omnilane native-context --workdir " + args.workdir)
|
|
273
|
+
print(aa_policy._json_line(decision), end="", file=sys.stderr)
|
|
274
|
+
return 3
|
|
275
|
+
plan = {"schema_version": 1, "executor": executor, "executor_reason": reason, "inherit": True,
|
|
276
|
+
"caller_identity_verified": decision["caller_identity_verified"],
|
|
277
|
+
"caller_identity_source": decision["caller_identity_source"],
|
|
278
|
+
"vendor": identity["vendor"], "model": identity["model"], "effort": "inherited",
|
|
279
|
+
"harness": ctx["harness"], "lane": args.lane, "mode": args.mode,
|
|
280
|
+
"workdir": args.workdir, "task": args.task,
|
|
281
|
+
"requirements": ctx["requirements"], "timeout": args.timeout,
|
|
282
|
+
"worker_contract": {
|
|
283
|
+
"no_nested_dispatch": True,
|
|
284
|
+
"isolation": ctx["requirements"]["isolation"],
|
|
285
|
+
"mode_is_task_intent": True,
|
|
286
|
+
"caller_enforces_deadline": True,
|
|
287
|
+
"model_override": False,
|
|
288
|
+
"inherit_caller_runtime": True,
|
|
289
|
+
"satisfies_lane_target": False,
|
|
290
|
+
},
|
|
291
|
+
"aa_policy": decision,
|
|
292
|
+
"state": "planned" if args.dry_run else "pending",
|
|
293
|
+
"job_id": None, "task_id": None, "agent_id": None,
|
|
294
|
+
"provider_invoked": False, "job_state_created": False,
|
|
295
|
+
"agent_strategy": "new", "existing_agent_id": None, "preserve_existing_context": False,
|
|
296
|
+
"new_agent_capacity": ctx.get("new_agent_capacity", "unknown")}
|
|
297
|
+
return publish_plan(args, plan, decision, registry, caller)
|
|
298
|
+
|
|
299
|
+
|
|
213
300
|
def stamp():
|
|
214
301
|
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
215
302
|
|
|
@@ -302,6 +389,8 @@ def route(args):
|
|
|
302
389
|
registry, registry_sha = aa_policy.load_registry(
|
|
303
390
|
args.policy, args.expected_registry_sha256
|
|
304
391
|
)
|
|
392
|
+
if getattr(args, "inherit", False):
|
|
393
|
+
return route_inherited(args, ctx, registry, registry_sha)
|
|
305
394
|
caller = None
|
|
306
395
|
caller_sha = None
|
|
307
396
|
if args.caller_context:
|
|
@@ -355,6 +444,10 @@ def route(args):
|
|
|
355
444
|
plan["worker_contract"].update(backend="collaboration.followup_task",
|
|
356
445
|
preserve_existing_context=True,
|
|
357
446
|
caller_rechecks_idle_before_followup=True)
|
|
447
|
+
return publish_plan(args, plan, policy_decision, registry, caller)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def publish_plan(args, plan, policy_decision, registry, caller):
|
|
358
451
|
if args.dry_run:
|
|
359
452
|
print(json_text(plan), end="")
|
|
360
453
|
return 0
|
|
@@ -393,6 +486,8 @@ def validate_completion(value, state):
|
|
|
393
486
|
for key in ("vendor", "model", "effort", "harness", "backend"):
|
|
394
487
|
text_value(runtime[key], "runtime " + key, 256)
|
|
395
488
|
for key in ("vendor", "model", "effort", "harness"):
|
|
489
|
+
if key == "effort" and state.get("inherit") is True:
|
|
490
|
+
continue # the host reports what it observed; nothing was requested
|
|
396
491
|
check(runtime[key] == state[key], "runtime " + key + " mismatch")
|
|
397
492
|
strategy = state.get("agent_strategy", "new")
|
|
398
493
|
if strategy == "reuse":
|
|
@@ -448,6 +543,10 @@ def job_command(args):
|
|
|
448
543
|
summary.update(agent_strategy=state.get("agent_strategy", "new"),
|
|
449
544
|
existing_agent_id=state.get("existing_agent_id"),
|
|
450
545
|
preserve_existing_context=state.get("preserve_existing_context", False))
|
|
546
|
+
if state.get("inherit"):
|
|
547
|
+
summary.update(inherit=True,
|
|
548
|
+
caller_identity_verified=state.get("caller_identity_verified", True),
|
|
549
|
+
satisfies_lane_target=False)
|
|
451
550
|
if args.action == "result":
|
|
452
551
|
summary["completion"] = state.get("completion")
|
|
453
552
|
if args.json:
|
|
@@ -463,8 +562,12 @@ def main():
|
|
|
463
562
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
464
563
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
465
564
|
p = sub.add_parser("route")
|
|
466
|
-
for key in ("home", "lane", "
|
|
565
|
+
for key in ("home", "lane", "workdir", "task"):
|
|
467
566
|
p.add_argument("--" + key, required=True)
|
|
567
|
+
for key in ("vendor", "model", "effort"):
|
|
568
|
+
p.add_argument("--" + key, default="")
|
|
569
|
+
p.add_argument("--inherit", action="store_true",
|
|
570
|
+
help="spawn a worker that inherits the caller's own model and effort")
|
|
468
571
|
p.add_argument("--executor", choices=("auto", "native", "cli"), required=True)
|
|
469
572
|
p.add_argument("--mode", choices=("advise", "work", "sysops"), required=True)
|
|
470
573
|
p.add_argument("--context")
|
|
@@ -488,6 +591,8 @@ def main():
|
|
|
488
591
|
p.add_argument("job_id")
|
|
489
592
|
p.add_argument("input", nargs="?")
|
|
490
593
|
args = parser.parse_args()
|
|
594
|
+
if args.command == "route" and not args.inherit and not args.vendor:
|
|
595
|
+
parser.error("--vendor is required unless --inherit is given")
|
|
491
596
|
try:
|
|
492
597
|
return route(args) if args.command == "route" else job_command(args)
|
|
493
598
|
except (ValueError, OSError, RecursionError, TypeError, KeyError) as error:
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Write the native capability file for the harness this runs under.
|
|
3
|
+
|
|
4
|
+
Dispatch only hands work to a harness's own sub-agent tool when the harness says
|
|
5
|
+
what that tool can do; without such a file every same-harness target goes out
|
|
6
|
+
through an external CLI. This writes the file from what is observable: the
|
|
7
|
+
caller's vendor, model and effort are read exactly as `omnilane whoami` reads
|
|
8
|
+
them. What cannot be observed from a shell is stated by the host that runs this,
|
|
9
|
+
with flags: `--inherits-caller-runtime` says its sub-agent tool, given no model
|
|
10
|
+
override, runs the caller's own model and effort. Where the identity cannot be
|
|
11
|
+
read at all, `--vendor` and `--model` are the host's statement of what it is; the
|
|
12
|
+
file then says so, and serves an inherited worker only. Prints the file's path.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
25
|
+
import caller_identity # noqa: E402
|
|
26
|
+
|
|
27
|
+
HARNESSES = {"codex": "codex", "claude": "claude-code", "grok": "grok-build", "gemini": "antigravity"}
|
|
28
|
+
UNVERIFIED_EFFORT = "unverified"
|
|
29
|
+
HOST_STATEMENT_HINT = ("omnilane: if this harness cannot be read, state what it is: omnilane native-context "
|
|
30
|
+
"--vendor VENDOR --model MODEL --inherits-caller-runtime (serves --inherit only)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build(vendor: str, model: str, effort: str | None, *, harness: str, workdirs: list[str],
|
|
34
|
+
modes: list[str], inherits: bool, verified: bool = True) -> dict:
|
|
35
|
+
value = {
|
|
36
|
+
"schema_version": 1,
|
|
37
|
+
"harness": harness,
|
|
38
|
+
"vendor": vendor,
|
|
39
|
+
"current_model": model,
|
|
40
|
+
"requirements": {"tools": [], "isolation": "shared-inherited", "lifecycle": "single-shot"},
|
|
41
|
+
"capabilities": [{
|
|
42
|
+
"model": model,
|
|
43
|
+
# An unrecorded effort matches no lane target; only --inherit can use this row.
|
|
44
|
+
"efforts": [effort or UNVERIFIED_EFFORT],
|
|
45
|
+
"modes": modes,
|
|
46
|
+
"workdirs": workdirs,
|
|
47
|
+
"tools": [],
|
|
48
|
+
"isolations": ["shared-inherited"],
|
|
49
|
+
"lifecycles": ["single-shot"],
|
|
50
|
+
}],
|
|
51
|
+
}
|
|
52
|
+
if effort:
|
|
53
|
+
value["current_effort"] = effort
|
|
54
|
+
if inherits:
|
|
55
|
+
value["inherits_caller_runtime"] = True
|
|
56
|
+
if not verified:
|
|
57
|
+
value["caller_identity_verified"] = False
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None, read_caller=caller_identity.read_caller) -> int:
|
|
62
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
63
|
+
parser.add_argument("--workdir", action="append", type=Path,
|
|
64
|
+
help="directory a native worker may be given; may repeat (default: cwd)")
|
|
65
|
+
parser.add_argument("--mode", action="append", choices=("advise", "work"),
|
|
66
|
+
help="task intent the host accepts; may repeat (default: advise)")
|
|
67
|
+
parser.add_argument("--harness", help="harness name (default: derived from the caller's vendor)")
|
|
68
|
+
parser.add_argument("--inherits-caller-runtime", action="store_true",
|
|
69
|
+
help="the host states that a sub-agent spawned with no model override "
|
|
70
|
+
"runs the caller's own model and effort")
|
|
71
|
+
parser.add_argument("--registry",
|
|
72
|
+
default=os.environ.get("OMNILANE_AA_POLICY_FILE")
|
|
73
|
+
or str(caller_identity.REPO / "config" / "aa-model-policy.json"))
|
|
74
|
+
parser.add_argument("--vendor", help="host-asserted vendor, used only when the identity cannot be read")
|
|
75
|
+
parser.add_argument("--model", help="host-asserted current model, used only when the identity cannot be read")
|
|
76
|
+
parser.add_argument("--out", type=Path, help="file to write (default: under ~/.omnilane)")
|
|
77
|
+
args = parser.parse_args(argv)
|
|
78
|
+
if bool(args.vendor) != bool(args.model):
|
|
79
|
+
print("omnilane: --vendor and --model go together", file=sys.stderr)
|
|
80
|
+
return 2
|
|
81
|
+
for name in (args.vendor, args.model):
|
|
82
|
+
if name and not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", name):
|
|
83
|
+
print(f"omnilane: invalid name: {name}", file=sys.stderr)
|
|
84
|
+
return 2
|
|
85
|
+
verified, recorded = True, None
|
|
86
|
+
try:
|
|
87
|
+
registry, _ = caller_identity.load_registry(args.registry)
|
|
88
|
+
_, (vendor, model, effort), _ = read_caller(os.getpid())
|
|
89
|
+
except (ValueError, OSError) as error:
|
|
90
|
+
if not args.vendor:
|
|
91
|
+
print(f"omnilane: cannot read the caller identity: {error}\n{HOST_STATEMENT_HINT}", file=sys.stderr)
|
|
92
|
+
return 3
|
|
93
|
+
print(f"omnilane: caller identity not verified ({error}); using the host's statement",
|
|
94
|
+
file=sys.stderr)
|
|
95
|
+
verified, vendor, model = False, args.vendor, args.model
|
|
96
|
+
else:
|
|
97
|
+
if args.vendor and (args.vendor != vendor or (model is not None and args.model != model)):
|
|
98
|
+
print(f"omnilane: this harness reads as {vendor}/{model}, not {args.vendor}/{args.model}",
|
|
99
|
+
file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
row, reason = caller_identity.resolve(registry, vendor, model, effort)
|
|
102
|
+
if row is not None:
|
|
103
|
+
# Codex spells its reasoning-off effort "none"; the registry stores it as null.
|
|
104
|
+
recorded = row["effort"] or ("none" if vendor == "codex" else None)
|
|
105
|
+
elif not (vendor == "codex" and model and effort is None
|
|
106
|
+
and caller_identity.floor_row(registry, vendor, model)):
|
|
107
|
+
if not args.vendor:
|
|
108
|
+
print(f"omnilane: cannot describe this harness: {reason}\n{HOST_STATEMENT_HINT}",
|
|
109
|
+
file=sys.stderr)
|
|
110
|
+
return 3
|
|
111
|
+
print(f"omnilane: caller identity not verified ({reason}); using the host's statement",
|
|
112
|
+
file=sys.stderr)
|
|
113
|
+
verified, model = False, args.model
|
|
114
|
+
workdirs = []
|
|
115
|
+
for path in args.workdir or [Path.cwd()]:
|
|
116
|
+
resolved = path.expanduser().resolve()
|
|
117
|
+
if not resolved.is_dir():
|
|
118
|
+
print(f"omnilane: not a directory: {path}", file=sys.stderr)
|
|
119
|
+
return 2
|
|
120
|
+
workdirs.append(str(resolved))
|
|
121
|
+
value = build(vendor, model, recorded,
|
|
122
|
+
harness=args.harness or HARNESSES.get(vendor, vendor),
|
|
123
|
+
workdirs=sorted(set(workdirs)), modes=sorted(set(args.mode or ["advise"])),
|
|
124
|
+
inherits=args.inherits_caller_runtime, verified=verified)
|
|
125
|
+
text = json.dumps(value, indent=2, sort_keys=True) + "\n"
|
|
126
|
+
out = args.out
|
|
127
|
+
if out is None:
|
|
128
|
+
home = Path(os.environ.get("OMNILANE_HOME") or Path.home() / ".omnilane")
|
|
129
|
+
digest = hashlib.sha256(text.encode()).hexdigest()[:12]
|
|
130
|
+
out = home / "native-context" / f"{value['harness']}--{model.replace('.', '-')}--{digest}.json"
|
|
131
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
staging = out.with_name(f".{out.name}.{os.getpid()}.tmp")
|
|
133
|
+
staging.write_text(text)
|
|
134
|
+
os.replace(staging, out)
|
|
135
|
+
note = "" if args.inherits_caller_runtime else (
|
|
136
|
+
"; pass --inherits-caller-runtime if this harness's sub-agent tool inherits your runtime")
|
|
137
|
+
print(f"omnilane: native context for {value['harness']} {model} "
|
|
138
|
+
f"(effort {recorded or 'unrecorded'}{'' if verified else ', identity host-asserted'}){note}",
|
|
139
|
+
file=sys.stderr)
|
|
140
|
+
print(out)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
raise SystemExit(main())
|
|
@@ -53,7 +53,10 @@ def offenders(overlay_path: Path) -> list[str]:
|
|
|
53
53
|
def main() -> None:
|
|
54
54
|
overlay_path = os.environ.get("OMNILANE_AA_TRANSPORT_OVERLAY", "")
|
|
55
55
|
if not overlay_path:
|
|
56
|
-
|
|
56
|
+
if os.environ.get("OMNILANE_AA_OPERATOR_ASSERTED_HUMAN") == "1":
|
|
57
|
+
emit("PASS", "no overlay configured; fine for a human operator, but a model caller "
|
|
58
|
+
"would be refused on every lane (README, 'Let your AI assistant drive omnilane')")
|
|
59
|
+
emit("WARN", "no overlay configured, so a model caller is refused on every lane (runtime-mapping-unverified). First install: probe_sweep.py --root ROOT, build_overlay.py --root ROOT, then export OMNILANE_AA_TRANSPORT_OVERLAY in local.sh; see the README, 'Let your AI assistant drive omnilane'")
|
|
57
60
|
if not Path(overlay_path).exists():
|
|
58
61
|
emit("FAIL", f"configured overlay is missing: {overlay_path}")
|
|
59
62
|
|
|
@@ -87,10 +90,29 @@ def main() -> None:
|
|
|
87
90
|
"record who answered")
|
|
88
91
|
|
|
89
92
|
stale = registry.get("_stale_transport_vendors", [])
|
|
93
|
+
# The gate hashes the path the overlay recorded. An update that installs beside
|
|
94
|
+
# the old executable leaves that path intact, so only this comparison sees it.
|
|
95
|
+
moved = []
|
|
96
|
+
try:
|
|
97
|
+
import resign
|
|
98
|
+
report = resign.detect(overlay, resign.current_anchors())
|
|
99
|
+
for vendor, entry in report.items():
|
|
100
|
+
# Only what the overlay actually pinned can have moved.
|
|
101
|
+
reasons = [reason for reason in entry["reasons"]
|
|
102
|
+
if reason.startswith("runs ") or reason.endswith(" changed")]
|
|
103
|
+
if reasons and entry["recorded_cli_path"] and vendor not in stale:
|
|
104
|
+
moved.append(f"{vendor}: {'; '.join(reasons)}")
|
|
105
|
+
except Exception: # noqa: BLE001 - a health line must never raise
|
|
106
|
+
moved = []
|
|
107
|
+
if moved and not stale:
|
|
108
|
+
emit("WARN", f"the runners no longer execute what the overlay pinned ({' | '.join(moved)}); "
|
|
109
|
+
f"run `omnilane resign`; still loading: {summary}{extra}")
|
|
90
110
|
if stale:
|
|
91
111
|
detail = "; ".join(o for o in offenders(Path(overlay_path))) or "unknown cause"
|
|
112
|
+
if moved:
|
|
113
|
+
detail += "; also moved without going stale: " + " | ".join(moved)
|
|
92
114
|
emit("WARN", f"stale vendor(s) {', '.join(stale)} degraded to unverified "
|
|
93
|
-
f"({detail}); still verified: {summary}{extra}")
|
|
115
|
+
f"({detail}); run `omnilane resign`; still verified: {summary}{extra}")
|
|
94
116
|
emit("PASS", f"verified mappings: {summary}{extra}")
|
|
95
117
|
|
|
96
118
|
|
package/scripts/lib/probe.py
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
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) -> dict:
|
|
96
|
+
"""Probe one vendor into root/evidence. Never raises for a probe that merely failed."""
|
|
97
|
+
home = home or Path.home()
|
|
98
|
+
entries = plan(vendor)
|
|
99
|
+
report = {"vendor": vendor, "outcome": "done", "passed": [], "failed": [], "detail": ""}
|
|
100
|
+
if (vendor in KEYCHAIN_VENDORS and sys.platform == "darwin"
|
|
101
|
+
and os.environ.get("OMNILANE_PROBE_ANY_SESSION") != "1"):
|
|
102
|
+
name = manager()
|
|
103
|
+
if name and name != "Aqua":
|
|
104
|
+
report.update(outcome="unprobeable",
|
|
105
|
+
detail=(f"this is a launchd {name} session (an ssh login is one); "
|
|
106
|
+
f"{vendor}'s credentials are in the login keychain, which only an "
|
|
107
|
+
"Aqua (GUI) session can read"))
|
|
108
|
+
return report
|
|
109
|
+
work = root / "work"
|
|
110
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
cli = "agy" if vendor == "gemini" else vendor
|
|
112
|
+
for index, entry in enumerate(entries):
|
|
113
|
+
app_root = None
|
|
114
|
+
app_data = None
|
|
115
|
+
if vendor == "gemini":
|
|
116
|
+
# prepare-agy-mode binds its private root to one workdir, so it is per sweep.
|
|
117
|
+
app_root = home / ".omnilane" / "agy-app" / f"probe-{root.name}-{entry['name']}"
|
|
118
|
+
prepared = subprocess.run(
|
|
119
|
+
[sys.executable, str(repo / "scripts/lib/prepare-agy-mode.py"), "--mode", "advise",
|
|
120
|
+
"--workdir", str(work), "--app-root", str(app_root),
|
|
121
|
+
"--gemini-dir", str(home / ".gemini")],
|
|
122
|
+
capture_output=True, text=True)
|
|
123
|
+
if prepared.returncode != 0:
|
|
124
|
+
report.update(outcome="failed",
|
|
125
|
+
detail=f"prepare-agy-mode failed: {prepared.stderr.strip()[-200:]}")
|
|
126
|
+
return report
|
|
127
|
+
app_data = prepared.stdout.strip()
|
|
128
|
+
argv = command(vendor, entry["model"], entry["effort"], app_data)
|
|
129
|
+
record = {}
|
|
130
|
+
for attempt in (1, 2):
|
|
131
|
+
record = run_probe(entry["name"], argv, root=root, vendor=cli,
|
|
132
|
+
expected_token=TOKENS[vendor], app_root=app_root)
|
|
133
|
+
if record.get("verdict") == "pass" or attempt == 2 or not TRANSIENT.search(_text(record)):
|
|
134
|
+
break
|
|
135
|
+
log(f"{entry['name']}: transient failure, retrying once")
|
|
136
|
+
if record.get("verdict") == "pass":
|
|
137
|
+
report["passed"].append(entry["config_id"])
|
|
138
|
+
log(f"{entry['name']} pass {record.get('evidence_tier')}")
|
|
139
|
+
continue
|
|
140
|
+
if NOT_AUTHENTICATED.search(_text(record)):
|
|
141
|
+
# Not a finding about the selector: keep it out of the evidence.
|
|
142
|
+
for suffix in ("json", "stdout", "stderr", "rollout", "cli_log"):
|
|
143
|
+
(root / "evidence" / f"{entry['name']}.{suffix}").unlink(missing_ok=True)
|
|
144
|
+
report.update(outcome="unprobeable",
|
|
145
|
+
detail=f"{cli} is not logged in on this host; log in, then re-run")
|
|
146
|
+
if index:
|
|
147
|
+
report["detail"] += f" ({index} earlier probe(s) had already answered)"
|
|
148
|
+
return report
|
|
149
|
+
report["failed"].append(entry["config_id"])
|
|
150
|
+
log(f"{entry['name']} fail {str(record.get('verdict_reason'))[:120]}")
|
|
151
|
+
if not report["passed"]:
|
|
152
|
+
report.update(outcome="failed", detail="no selector of this vendor passed")
|
|
153
|
+
return report
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main(argv: list[str] | None = None) -> int:
|
|
157
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
158
|
+
parser.add_argument("--root", type=Path, required=True, help="sweep root to write evidence into")
|
|
159
|
+
parser.add_argument("--vendor", action="append", choices=VENDORS,
|
|
160
|
+
help="vendor to probe; may repeat (default: all four)")
|
|
161
|
+
parser.add_argument("--plan", action="store_true", help="print the commands and probe nothing")
|
|
162
|
+
args = parser.parse_args(argv)
|
|
163
|
+
vendors = args.vendor or list(VENDORS)
|
|
164
|
+
if args.plan:
|
|
165
|
+
for vendor in vendors:
|
|
166
|
+
for entry in plan(vendor):
|
|
167
|
+
print(json.dumps({"name": entry["name"], "argv": command(
|
|
168
|
+
vendor, entry["model"], entry["effort"], "<app-data>")}, ensure_ascii=False))
|
|
169
|
+
return 0
|
|
170
|
+
reports = [sweep(vendor, args.root.expanduser()) for vendor in vendors]
|
|
171
|
+
print(json.dumps(reports, ensure_ascii=False, indent=2))
|
|
172
|
+
return 0 if all(report["outcome"] == "done" for report in reports) else 1
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
raise SystemExit(main())
|