omnilane 0.41.1 → 0.42.2
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 +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +38 -1
- package/README.ja.md +27 -2
- package/README.ko.md +27 -2
- package/README.md +89 -16
- package/README.zh-CN.md +27 -2
- package/README.zh-TW.md +69 -11
- package/VERSION +1 -1
- package/config/aa-model-policy.json +3046 -0
- package/docs/completion-wakeup.md +126 -0
- package/docs/native-executor.md +264 -0
- package/docs/release-notes-0.42.2.md +31 -0
- package/hooks/routing-instruction.md +101 -40
- package/package.json +6 -2
- package/plugin.json +2 -2
- package/routing.yaml +7 -7
- package/scripts/completion-wakeup.py +390 -0
- package/scripts/dispatch.sh +240 -18
- package/scripts/jobs.sh +58 -15
- package/scripts/lib/aa_policy.py +482 -0
- package/scripts/lib/aa_retry.py +77 -0
- package/scripts/lib/common.sh +59 -0
- package/scripts/lib/job-worker.sh +2 -0
- package/scripts/lib/native.py +507 -0
- package/scripts/runners/run-grok.sh +16 -2
- package/scripts/runners/run-vote.sh +3 -0
- package/skills/omnilane/SKILL.md +120 -31
|
@@ -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())
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
# omnilane runner: Grok Build CLI
|
|
4
4
|
# Usage: run-grok.sh MODE WORKDIR MODEL EFFORT PROMPT_FILE OUTPUT_FILE
|
|
5
|
-
# EFFORT is
|
|
5
|
+
# Explicit EFFORT is passed through Grok CLI's reasoning-effort selector.
|
|
6
6
|
|
|
7
7
|
source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
|
|
8
8
|
|
|
9
9
|
MODE="$1"; WORKDIR="$2"; MODEL="$3"; EFFORT="$4"; PROMPT_FILE="$5"; OUTPUT_FILE="$6"
|
|
10
|
-
|
|
10
|
+
EFFORT_ARGS=()
|
|
11
|
+
case "$EFFORT" in
|
|
12
|
+
low|medium|high|xhigh) EFFORT_ARGS=(--reasoning-effort "$EFFORT") ;;
|
|
13
|
+
-|"") ;; # Unspecified effort has no scored runtime mapping.
|
|
14
|
+
*) echo "omnilane: invalid Grok reasoning effort '$EFFORT'" >&2; exit 2 ;;
|
|
15
|
+
esac
|
|
11
16
|
|
|
12
17
|
GROK_BIN="${GROK_BIN:-grok}"
|
|
13
18
|
RUN_TIMEOUT="${OMNILANE_TIMEOUT:-600}"
|
|
@@ -78,6 +83,10 @@ if [[ "$MODE" != "sysops" && -n "$LIVE_INBOX" ]]; then
|
|
|
78
83
|
exit 2
|
|
79
84
|
fi
|
|
80
85
|
if [[ -n "$LIVE_INBOX" && -p "$LIVE_INBOX" ]]; then
|
|
86
|
+
if [[ ${#EFFORT_ARGS[@]} -gt 0 ]]; then
|
|
87
|
+
echo "omnilane: explicit Grok reasoning effort is not verified for live ACP; use single-shot" >&2
|
|
88
|
+
exit 2
|
|
89
|
+
fi
|
|
81
90
|
EVENTS_FILE="${OUTPUT_FILE}.events.jsonl"
|
|
82
91
|
STDERR_FILE="${OUTPUT_FILE}.stderr.log"
|
|
83
92
|
PROGRESS_FILE="${OUTPUT_FILE}.progress.log"
|
|
@@ -145,6 +154,7 @@ fi
|
|
|
145
154
|
ARGS=(--cwd "$WORKDIR" --model "$MODEL"
|
|
146
155
|
--no-memory --no-subagents --no-plan --no-alt-screen
|
|
147
156
|
--output-format plain --verbatim --prompt-file "$PROMPT_FILE")
|
|
157
|
+
[[ ${#EFFORT_ARGS[@]} -eq 0 ]] || ARGS+=("${EFFORT_ARGS[@]}")
|
|
148
158
|
[[ ${#THREAD_ARGS[@]} -eq 0 ]] || ARGS+=("${THREAD_ARGS[@]}")
|
|
149
159
|
ARGS+=("${MODE_ARGS[@]}")
|
|
150
160
|
# Web/X search stays ON by default for advise and sysops.
|
|
@@ -155,6 +165,10 @@ fi
|
|
|
155
165
|
# Grok intermittently emits empty output on large inputs; retry until it speaks.
|
|
156
166
|
RC=0; attempt=1
|
|
157
167
|
while [[ "$attempt" -le "$MAX_ATTEMPTS" ]]; do
|
|
168
|
+
# Grok can retry provider startup internally, so each attempt rechecks the
|
|
169
|
+
# frozen decision. In model-caller mode this also rejects Grok effort rows:
|
|
170
|
+
# the current runner accepts EFFORT only for parity and discards it.
|
|
171
|
+
aa_policy_gate grok "$MODEL" "$EFFORT" || exit $?
|
|
158
172
|
set +e
|
|
159
173
|
OMNILANE_DEPTH=1 run_with_timeout "$RUN_TIMEOUT" \
|
|
160
174
|
"$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
|