omnilane 0.41.1 → 0.42.1

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,507 @@
1
+ #!/usr/bin/env python3
2
+ """Caller-owned native handoff protocol. No provider discovery or agent spawning.
3
+
4
+ The shell resolves a routing row before invoking this module. Exit 10 from
5
+ route means auto chose CLI; all other errors fail closed. Native state is a
6
+ single atomically replaced JSON record, serialized by a per-job advisory lock.
7
+ """
8
+
9
+ import argparse
10
+ import datetime
11
+ import fcntl
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import re
16
+ import secrets
17
+ import stat
18
+ import sys
19
+
20
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
21
+ import aa_policy # noqa: E402
22
+
23
+
24
+ MAX_BYTES = 262144
25
+ JOB_ID = re.compile(r"[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+\Z")
26
+ IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}\Z")
27
+
28
+
29
+ def check(condition, message):
30
+ if not condition:
31
+ raise ValueError(message)
32
+
33
+
34
+ def text_value(value, label, limit=4096, multiline=False):
35
+ check(isinstance(value, str) and bool(value.strip()) and len(value) <= limit,
36
+ f"invalid {label}")
37
+ check(all(ord(c) >= 32 or (multiline and c in "\n\t") for c in value)
38
+ and "\x7f" not in value, f"control character in {label}")
39
+ return value
40
+
41
+
42
+ def identifier(value, label):
43
+ text_value(value, label, 256)
44
+ check(IDENTIFIER.fullmatch(value), f"invalid {label}")
45
+ return value
46
+
47
+
48
+ def fields(value, required, optional=()):
49
+ check(isinstance(value, dict), "expected JSON object")
50
+ check(set(required) <= value.keys() and value.keys() <= set(required) | set(optional),
51
+ "missing or unknown JSON fields")
52
+
53
+
54
+ def string_list(value, label, nonempty=True):
55
+ check(isinstance(value, list) and len(value) <= 128 and (value or not nonempty),
56
+ f"invalid {label}")
57
+ for item in value:
58
+ text_value(item, label)
59
+ check(len(set(value)) == len(value), f"duplicate {label}")
60
+ return value
61
+
62
+
63
+ def unique_object(pairs):
64
+ result = {}
65
+ for key, value in pairs:
66
+ check(key not in result, "duplicate JSON key")
67
+ result[key] = value
68
+ return result
69
+
70
+
71
+ def read_json(path):
72
+ # Never follow a submitted symlink (context, completion, or job state).
73
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
74
+ with os.fdopen(fd, "r", encoding="utf-8") as stream:
75
+ info = os.fstat(stream.fileno())
76
+ check(stat.S_ISREG(info.st_mode) and info.st_size <= MAX_BYTES, "invalid JSON file")
77
+ raw = stream.read(MAX_BYTES + 1)
78
+ check(len(raw.encode("utf-8")) <= MAX_BYTES, "JSON file too large")
79
+ return json.loads(raw, object_pairs_hook=unique_object,
80
+ parse_constant=lambda _: (_ for _ in ()).throw(ValueError("invalid JSON number")))
81
+
82
+
83
+ def canonical_dir(value):
84
+ text_value(value, "workdir")
85
+ path = Path(value)
86
+ check(path.is_absolute() and path.is_dir(), "workdir must be an existing absolute directory")
87
+ return str(path.resolve(strict=True))
88
+
89
+
90
+ def capability_context(path):
91
+ ctx = read_json(path)
92
+ fields(ctx, ("schema_version", "harness", "vendor", "capabilities", "requirements"),
93
+ ("current_model", "current_effort", "agent_strategy", "existing_agent",
94
+ "preserve_existing_context", "new_agent_capacity"))
95
+ check(type(ctx["schema_version"]) is int and ctx["schema_version"] == 1, "unsupported context version")
96
+ identifier(ctx["harness"], "harness")
97
+ identifier(ctx["vendor"], "vendor")
98
+ if "current_model" in ctx:
99
+ text_value(ctx["current_model"], "current_model", 256)
100
+ if "current_effort" in ctx:
101
+ text_value(ctx["current_effort"], "current_effort", 256)
102
+ check(ctx.get("agent_strategy", "new") in ("new", "reuse"), "invalid agent strategy")
103
+ check(ctx.get("new_agent_capacity", "unknown") in ("available", "exhausted", "unknown"),
104
+ "invalid new agent capacity")
105
+ if "preserve_existing_context" in ctx:
106
+ check(type(ctx["preserve_existing_context"]) is bool, "invalid preserve_existing_context")
107
+ if "existing_agent" in ctx:
108
+ agent = ctx["existing_agent"]
109
+ fields(agent, ("agent_id", "vendor", "model", "effort", "harness", "state", "observed_by", "evidence"))
110
+ text_value(agent["agent_id"], "existing agent id", 256)
111
+ check(re.fullmatch(r"[A-Za-z0-9_/][A-Za-z0-9._:/-]{0,255}", agent["agent_id"]),
112
+ "invalid existing agent id")
113
+ for key in ("vendor", "model", "effort", "harness"):
114
+ text_value(agent[key], "existing agent " + key, 256)
115
+ check(agent["state"] in ("idle", "busy", "unknown"), "invalid existing agent state")
116
+ check(agent["observed_by"] == "caller", "existing agent must be caller-observed")
117
+ string_list(agent["evidence"], "existing agent evidence")
118
+ req = ctx["requirements"]
119
+ fields(req, ("tools", "isolation", "lifecycle"))
120
+ string_list(req["tools"], "required tools", nonempty=False)
121
+ identifier(req["isolation"], "isolation")
122
+ identifier(req["lifecycle"], "lifecycle")
123
+ caps = ctx["capabilities"]
124
+ check(isinstance(caps, list) and 0 < len(caps) <= 128, "invalid capabilities")
125
+ for cap in caps:
126
+ fields(cap, ("model", "efforts", "modes", "workdirs", "tools", "isolations", "lifecycles"),
127
+ ("agent_strategy", "existing_agent_id"))
128
+ check(cap.get("agent_strategy", "new") in ("new", "reuse"), "invalid capability strategy")
129
+ if "existing_agent_id" in cap:
130
+ text_value(cap["existing_agent_id"], "capability existing agent id", 256)
131
+ text_value(cap["model"], "model", 256)
132
+ for key in ("efforts", "modes", "workdirs", "isolations", "lifecycles"):
133
+ string_list(cap[key], key)
134
+ string_list(cap["tools"], "tools", nonempty=False)
135
+ cap["workdirs"] = [canonical_dir(d) for d in cap["workdirs"]]
136
+ return ctx
137
+
138
+
139
+ def choose(args, ctx):
140
+ """Exact match on one capability row, never unions across different rows."""
141
+ if args.executor == "cli":
142
+ return "cli", "forced-cli", args.model
143
+ if ctx is None:
144
+ return "cli", "no-native-context", args.model
145
+ if args.vendor in ("vote", "exec", "off"):
146
+ return "cli", "cli-only-routing-kind", args.model
147
+ if args.background or args.session != "auto" or args.thread:
148
+ return "cli", "cli-session-lifecycle", args.model
149
+ if args.job_timeout or args.idle_timeout:
150
+ return "cli", "cli-watchdog-required", args.model
151
+ if args.mode == "sysops":
152
+ return "cli", "sysops-requires-cli", args.model
153
+ if ctx["vendor"] != args.vendor:
154
+ return "cli", "vendor-mismatch", args.model
155
+ model = args.model
156
+ if model in ("", "-"):
157
+ model = ctx.get("current_model", "")
158
+ if not model:
159
+ return "cli", "unknown-current-model", args.model
160
+ caps = [cap for cap in ctx["capabilities"] if cap["model"] == model]
161
+ if not caps:
162
+ return "cli", "model-mismatch", model
163
+ if args.effort in ("", "-"):
164
+ return "cli", "unknown-effort", model
165
+ req = ctx["requirements"]
166
+ # collaboration.spawn_agent inherits the caller's tools and filesystem.
167
+ # advise/work express task intent; neither creates an OS sandbox here.
168
+ isolation = req["isolation"]
169
+ if isolation != "shared-inherited" or req["lifecycle"] != "single-shot":
170
+ return "cli", "unsupported-isolation-or-lifecycle", model
171
+ strategy = ctx.get("agent_strategy", "new")
172
+ if strategy == "reuse":
173
+ if ctx.get("preserve_existing_context") is not True:
174
+ return "cli", "reuse-preserve-context-required", model
175
+ agent = ctx.get("existing_agent")
176
+ if agent is None:
177
+ return "cli", "reuse-existing-agent-required", model
178
+ if agent["state"] != "idle":
179
+ return "cli", "reuse-agent-not-idle", model
180
+ if args.vendor != "codex":
181
+ return "cli", "reuse-backend-unsupported", model
182
+ if ctx.get("current_model") != model or ctx.get("current_effort") != args.effort:
183
+ return "cli", "reuse-current-runtime-mismatch", model
184
+ expected = {"vendor": args.vendor, "model": model, "effort": args.effort, "harness": ctx["harness"]}
185
+ if any(agent[key] != value for key, value in expected.items()):
186
+ return "cli", "reuse-agent-runtime-mismatch", model
187
+ caps = [cap for cap in caps if cap.get("agent_strategy", "new") == "reuse"
188
+ and cap.get("existing_agent_id") == agent["agent_id"]]
189
+ else:
190
+ if "existing_agent" in ctx or ctx.get("preserve_existing_context") is True:
191
+ return "cli", "reuse-strategy-required", model
192
+ if ctx.get("new_agent_capacity") == "exhausted":
193
+ return "cli", "new-agent-capacity-exhausted", model
194
+ caps = [cap for cap in caps if cap.get("agent_strategy", "new") == "new"
195
+ and "existing_agent_id" not in cap]
196
+ if not caps:
197
+ return "cli", "agent-strategy-capability-mismatch", model
198
+ checks = (
199
+ ("effort-mismatch", lambda c: args.effort in c["efforts"]),
200
+ ("mode-mismatch", lambda c: args.mode in c["modes"]),
201
+ ("workdir-mismatch", lambda c: args.workdir in c["workdirs"]),
202
+ ("tools-mismatch", lambda c: set(req["tools"]) <= set(c["tools"])),
203
+ ("isolation-mismatch", lambda c: isolation in c["isolations"]),
204
+ ("lifecycle-mismatch", lambda c: "single-shot" in c["lifecycles"]),
205
+ )
206
+ for reason, predicate in checks:
207
+ caps = [cap for cap in caps if predicate(cap)]
208
+ if not caps:
209
+ return "cli", reason, model
210
+ return "native", "exact-idle-reuse-match" if strategy == "reuse" else "exact-capability-match", model
211
+
212
+
213
+ def stamp():
214
+ return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
215
+
216
+
217
+ def write_new(path, content):
218
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
219
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
220
+ stream.write(content)
221
+ stream.flush()
222
+ os.fsync(stream.fileno())
223
+
224
+
225
+ def json_text(value):
226
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n"
227
+
228
+
229
+ def atomic_json(path, value):
230
+ content = json_text(value)
231
+ check(len(content.encode("utf-8")) <= MAX_BYTES, "public record too large")
232
+ temp = path.with_name("." + path.name + "." + secrets.token_hex(8))
233
+ write_new(temp, content)
234
+ try:
235
+ os.replace(temp, path)
236
+ finally:
237
+ if temp.exists():
238
+ temp.unlink()
239
+
240
+
241
+ def cleanup_staging_dir(path):
242
+ """Remove only files from a staging directory created by this process."""
243
+ try:
244
+ children = list(path.iterdir())
245
+ except FileNotFoundError:
246
+ return
247
+ for child in children:
248
+ # Never recurse or follow links during cleanup. A surprising entry is
249
+ # left hidden rather than risking deletion of unrelated state.
250
+ if child.is_symlink() or child.is_file():
251
+ child.unlink()
252
+ else:
253
+ return
254
+ path.rmdir()
255
+
256
+
257
+ def publish_job(root, job_id, plan, metadata, registry=None, caller=None, registry_source=None):
258
+ """Initialize privately, then publish one complete job directory."""
259
+ job = root / job_id
260
+ staging = root / (".native-stage-" + job_id + "-" + secrets.token_hex(8))
261
+ staging.mkdir(mode=0o700)
262
+ try:
263
+ if registry is not None:
264
+ aa_policy.publish_context(staging, registry, caller, plan["aa_policy"], registry_source)
265
+ write_new(staging / "native.lock", "")
266
+ write_new(staging / "task.txt", plan["task"])
267
+ write_new(staging / "meta.json", json_text(metadata))
268
+ write_new(staging / "native.json", json_text(plan))
269
+
270
+ publish_lock_path = root / ".native-publish.lock"
271
+ publish_fd = os.open(
272
+ publish_lock_path,
273
+ os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK,
274
+ 0o600,
275
+ )
276
+ with os.fdopen(publish_fd, "r+") as publish_lock:
277
+ check(stat.S_ISREG(os.fstat(publish_lock.fileno()).st_mode),
278
+ "invalid native publication lock")
279
+ fcntl.flock(publish_lock, fcntl.LOCK_EX)
280
+ check(not job.exists() and not job.is_symlink(), "job ID collision")
281
+ os.rename(staging, job)
282
+ finally:
283
+ cleanup_staging_dir(staging)
284
+
285
+
286
+ def jobs_root(home, create=False):
287
+ home = Path(home).absolute()
288
+ root = home / "jobs"
289
+ check(not home.is_symlink() and not root.is_symlink(), "unsafe jobs store")
290
+ if create:
291
+ home.mkdir(mode=0o700, parents=True, exist_ok=True)
292
+ root.mkdir(mode=0o700, exist_ok=True)
293
+ check(root.is_dir(), "missing jobs store")
294
+ return root
295
+
296
+
297
+ def route(args):
298
+ check(not (args.caller_context and args.operator_asserted_human),
299
+ "caller context and operator assertion are mutually exclusive")
300
+ ctx = capability_context(args.context) if args.context else None
301
+ args.workdir = canonical_dir(args.workdir)
302
+ registry, registry_sha = aa_policy.load_registry(
303
+ args.policy, args.expected_registry_sha256
304
+ )
305
+ caller = None
306
+ caller_sha = None
307
+ if args.caller_context:
308
+ caller, caller_sha = aa_policy.load_caller(
309
+ args.caller_context, registry, args.expected_caller_sha256
310
+ )
311
+ policy_decision = aa_policy.decide(
312
+ registry,
313
+ registry_sha,
314
+ vendor=args.vendor,
315
+ model=args.model,
316
+ effort=None if args.effort in ("", "-") else args.effort,
317
+ caller=caller,
318
+ caller_sha256=caller_sha,
319
+ operator_asserted_human=args.operator_asserted_human,
320
+ target_config=args.target_config,
321
+ )
322
+ if not policy_decision["allowed"]:
323
+ print(aa_policy._json_line(policy_decision), end="", file=sys.stderr)
324
+ return 3
325
+ executor, reason, model = choose(args, ctx)
326
+ if executor == "cli":
327
+ check(args.executor != "native", "native capability rejected: " + reason)
328
+ # Preserve an inherited exact model on CLI fallback too.
329
+ print(json_text({"reason": reason, "model": model}).strip())
330
+ return 10
331
+ plan = {"schema_version": 1, "executor": executor, "executor_reason": reason,
332
+ "vendor": args.vendor, "model": model, "effort": args.effort,
333
+ "harness": ctx["harness"], "lane": args.lane, "mode": args.mode,
334
+ "workdir": args.workdir, "task": args.task,
335
+ "requirements": ctx["requirements"], "timeout": args.timeout,
336
+ "worker_contract": {
337
+ "no_nested_dispatch": True,
338
+ "isolation": ctx["requirements"]["isolation"],
339
+ "mode_is_task_intent": True,
340
+ "caller_enforces_deadline": True,
341
+ },
342
+ "aa_policy": policy_decision,
343
+ "state": "planned" if args.dry_run else "pending",
344
+ "job_id": None, "task_id": None, "agent_id": None,
345
+ "provider_invoked": False, "job_state_created": False}
346
+ strategy = ctx.get("agent_strategy", "new")
347
+ plan.update(agent_strategy=strategy, existing_agent_id=None,
348
+ preserve_existing_context=strategy == "reuse",
349
+ new_agent_capacity=ctx.get("new_agent_capacity", "unknown"))
350
+ if strategy == "reuse":
351
+ agent = ctx["existing_agent"]
352
+ plan.update(existing_agent_id=agent["agent_id"], agent_id=agent["agent_id"],
353
+ reuse_observation={"state": agent["state"], "observed_by": "caller",
354
+ "evidence": agent["evidence"]})
355
+ plan["worker_contract"].update(backend="collaboration.followup_task",
356
+ preserve_existing_context=True,
357
+ caller_rechecks_idle_before_followup=True)
358
+ if args.dry_run:
359
+ print(json_text(plan), end="")
360
+ return 0
361
+ if args.task == "-":
362
+ plan["task"] = sys.stdin.read(MAX_BYTES + 1)
363
+ text_value(plan["task"], "task", 65536, multiline=True)
364
+ job_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + f"-{os.getpid()}-{secrets.randbelow(10**9)}"
365
+ plan.update(job_id=job_id, task_id=job_id, created=stamp(), job_state_created=True)
366
+ # Validate size before creating any job state.
367
+ check(len(json_text(plan).encode("utf-8")) <= MAX_BYTES, "handoff too large")
368
+ metadata = {k: plan[k] for k in ("lane", "vendor", "model", "effort", "mode", "workdir", "executor", "executor_reason")}
369
+ metadata.update(aa_policy_code=policy_decision["code"],
370
+ aa_effective_ceiling=policy_decision["effective_ceiling"])
371
+ metadata.update(started=plan["created"], session_mode="native")
372
+ check(len(json_text(metadata).encode("utf-8")) <= 4096, "metadata too large")
373
+ root = jobs_root(args.home, create=True)
374
+ if policy_decision.get("child_context") is not None:
375
+ plan["worker_contract"]["caller_context_path"] = str(root / job_id / "aa-child-context.json")
376
+ plan["worker_contract"]["caller_context"] = policy_decision["child_context"]
377
+ publish_job(root, job_id, plan, metadata, registry, caller, args.policy)
378
+ print(json_text(plan), end="")
379
+ return 0
380
+
381
+
382
+ def validate_completion(value, state):
383
+ fields(value, ("schema_version", "job_id", "agent_id", "runtime", "outcome", "result", "evidence"),
384
+ ("agent_strategy",))
385
+ check(type(value["schema_version"]) is int and value["schema_version"] == 1, "unsupported completion version")
386
+ check(value["job_id"] == state["job_id"], "completion job ID mismatch")
387
+ # Hosts may expose a canonical task name such as /root/reviewer as the
388
+ # agent identifier. It is public metadata only, never a path or a PID.
389
+ text_value(value["agent_id"], "agent_id", 256)
390
+ check(re.fullmatch(r"[A-Za-z0-9_/][A-Za-z0-9._:/-]{0,255}", value["agent_id"]), "invalid agent_id")
391
+ runtime = value["runtime"]
392
+ fields(runtime, ("vendor", "model", "effort", "harness", "backend"))
393
+ for key in ("vendor", "model", "effort", "harness", "backend"):
394
+ text_value(runtime[key], "runtime " + key, 256)
395
+ for key in ("vendor", "model", "effort", "harness"):
396
+ check(runtime[key] == state[key], "runtime " + key + " mismatch")
397
+ strategy = state.get("agent_strategy", "new")
398
+ if strategy == "reuse":
399
+ check(value.get("agent_strategy") == "reuse", "reuse completion strategy required")
400
+ check(value["agent_id"] == state["existing_agent_id"], "reuse agent ID mismatch")
401
+ check(runtime["backend"] == "collaboration.followup_task", "reuse backend mismatch")
402
+ else:
403
+ check(value.get("agent_strategy", "new") == "new", "new-agent completion strategy mismatch")
404
+ check(runtime["backend"] != "collaboration.followup_task", "reuse backend on new-agent job")
405
+ check(value["outcome"] in ("success", "failure"), "invalid outcome")
406
+ text_value(value["result"], "result", 65536, multiline=True)
407
+ string_list(value["evidence"], "evidence")
408
+ return value
409
+
410
+
411
+ def job_command(args):
412
+ check(JOB_ID.fullmatch(args.job_id), "invalid job ID")
413
+ job = jobs_root(args.home) / args.job_id
414
+ check(job.is_dir() and not job.is_symlink(), "invalid job directory")
415
+ lock_fd = os.open(job / "native.lock", os.O_RDWR | os.O_NOFOLLOW | os.O_NONBLOCK)
416
+ with os.fdopen(lock_fd, "r+") as lock:
417
+ check(stat.S_ISREG(os.fstat(lock.fileno()).st_mode), "invalid native lock")
418
+ fcntl.flock(lock, fcntl.LOCK_EX)
419
+ state = read_json(job / "native.json")
420
+ check(isinstance(state, dict) and state.get("job_id") == args.job_id
421
+ and state.get("executor") == "native"
422
+ and state.get("state") in ("pending", "completed", "cancelled"), "invalid native state")
423
+ if args.action in ("complete-native", "cancel"):
424
+ check(state["state"] == "pending", "native job is already terminal")
425
+ if args.action == "complete-native":
426
+ check(args.input is not None, "completion input file required")
427
+ value = validate_completion(read_json(args.input), state)
428
+ state.update(state="completed", agent_id=value["agent_id"], completion=value,
429
+ exit_code=0 if value["outcome"] == "success" else 1, finished=stamp())
430
+ else:
431
+ check(args.input is None, "unexpected cancel input")
432
+ # There is deliberately no PID or signal here. The caller owns
433
+ # any agent already spawned and must stop it with its own tool.
434
+ state.update(state="cancelled", exit_code=143, finished=stamp())
435
+ atomic_json(job / "native.json", state)
436
+ pending = state["state"] == "pending"
437
+ public_state = "pending" if pending else "cancelled" if state["state"] == "cancelled" else "done"
438
+ if args.action == "list-state":
439
+ print(public_state)
440
+ return 0
441
+ if args.action == "result":
442
+ check(not pending, "native job pending; ingest caller result first")
443
+ summary = {"id": args.job_id, "state": public_state, "native_state": state["state"],
444
+ "executor": "native", "executor_reason": state["executor_reason"],
445
+ "exit_code": state.get("exit_code"), "agent_id": state["agent_id"],
446
+ "vendor": state["vendor"], "model": state["model"], "effort": state["effort"],
447
+ "harness": state["harness"]}
448
+ summary.update(agent_strategy=state.get("agent_strategy", "new"),
449
+ existing_agent_id=state.get("existing_agent_id"),
450
+ preserve_existing_context=state.get("preserve_existing_context", False))
451
+ if args.action == "result":
452
+ summary["completion"] = state.get("completion")
453
+ if args.json:
454
+ print(json_text({"schema_version": 1, "command": args.action, "ok": True, "job": summary}), end="")
455
+ elif args.action == "result":
456
+ print(state.get("completion", {}).get("result", "cancelled by caller"))
457
+ else:
458
+ print(json_text(summary), end="")
459
+ return state.get("exit_code", 0) if args.action == "result" else 0
460
+
461
+
462
+ def main():
463
+ parser = argparse.ArgumentParser(description=__doc__)
464
+ sub = parser.add_subparsers(dest="command", required=True)
465
+ p = sub.add_parser("route")
466
+ for key in ("home", "lane", "vendor", "model", "effort", "workdir", "task"):
467
+ p.add_argument("--" + key, required=True)
468
+ p.add_argument("--executor", choices=("auto", "native", "cli"), required=True)
469
+ p.add_argument("--mode", choices=("advise", "work", "sysops"), required=True)
470
+ p.add_argument("--context")
471
+ p.add_argument("--policy", required=True)
472
+ p.add_argument("--caller-context")
473
+ p.add_argument("--operator-asserted-human", action="store_true")
474
+ p.add_argument("--expected-registry-sha256")
475
+ p.add_argument("--expected-caller-sha256")
476
+ p.add_argument("--target-config")
477
+ p.add_argument("--session", default="auto")
478
+ p.add_argument("--thread", default="")
479
+ p.add_argument("--timeout", type=int, default=600)
480
+ p.add_argument("--job-timeout", default="")
481
+ p.add_argument("--idle-timeout", default="")
482
+ p.add_argument("--background", action="store_true")
483
+ p.add_argument("--dry-run", action="store_true")
484
+ p = sub.add_parser("job")
485
+ p.add_argument("--home", required=True)
486
+ p.add_argument("--json", action="store_true")
487
+ p.add_argument("action", choices=("status", "result", "cancel", "complete-native", "list-state"))
488
+ p.add_argument("job_id")
489
+ p.add_argument("input", nargs="?")
490
+ args = parser.parse_args()
491
+ try:
492
+ return route(args) if args.command == "route" else job_command(args)
493
+ except (ValueError, OSError, RecursionError, TypeError, KeyError) as error:
494
+ # Avoid echoing submitted JSON, task text, result text, or capability data.
495
+ if isinstance(error, (OSError, UnicodeError, json.JSONDecodeError, RecursionError, TypeError, KeyError)):
496
+ message = "invalid or inaccessible native protocol input/state"
497
+ else:
498
+ message = str(error)
499
+ if getattr(args, "json", False):
500
+ print(json_text({"schema_version": 1, "command": args.action, "ok": False, "error": message}), end="")
501
+ else:
502
+ print("omnilane: " + message, file=sys.stderr)
503
+ return 2
504
+
505
+
506
+ if __name__ == "__main__":
507
+ sys.exit(main())
@@ -155,6 +155,10 @@ fi
155
155
  # Grok intermittently emits empty output on large inputs; retry until it speaks.
156
156
  RC=0; attempt=1
157
157
  while [[ "$attempt" -le "$MAX_ATTEMPTS" ]]; do
158
+ # Grok can retry provider startup internally, so each attempt rechecks the
159
+ # frozen decision. In model-caller mode this also rejects Grok effort rows:
160
+ # the current runner accepts EFFORT only for parity and discards it.
161
+ aa_policy_gate grok "$MODEL" "$EFFORT" || exit $?
158
162
  set +e
159
163
  OMNILANE_DEPTH=1 run_with_timeout "$RUN_TIMEOUT" \
160
164
  "$GROK_BIN" "${ARGS[@]}" > "${OUTPUT_FILE}.tmp" 2> "${OUTPUT_FILE}.stderr.log"
@@ -42,6 +42,9 @@ run_voter() { # vendor, prompt_file, out_file -> rc
42
42
  local v="$1" pf="$2" out="$3" spec model effort rc
43
43
  spec="$(voter_spec "$v")" || return 3
44
44
  model="${spec%%$'\t'*}"; effort="${spec##*$'\t'}"
45
+ # Each round and each constituent gets its own decision immediately before
46
+ # the child runner. The panel-level check in dispatch.sh is not a substitute.
47
+ aa_policy_gate "$v" "$model" "$effort" || return $?
45
48
  if [[ "$v" == "codex" ]]; then
46
49
  acquire_cwd_lock codex "$WORKDIR"
47
50
  trap 'release_cwd_lock; cleanup_temp_files' EXIT