omnilane 0.34.0 → 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.
- package/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +71 -1
- package/README.ja.md +63 -33
- package/README.ko.md +63 -32
- package/README.md +147 -86
- package/README.zh-CN.md +61 -30
- package/README.zh-TW.md +124 -75
- package/VERSION +1 -1
- package/config/aa-model-policy.json +3046 -0
- package/docs/aa-model-coverage-2026-09-05.json +29204 -0
- package/docs/completion-wakeup.md +126 -0
- package/docs/model-capabilities-2026-09.md +380 -0
- package/docs/native-executor.md +264 -0
- package/docs/release-notes-0.42.1.md +32 -0
- package/hooks/routing-instruction.md +101 -40
- package/package.json +8 -2
- package/plugin.json +2 -2
- package/routing.local.yaml.example +8 -3
- package/routing.yaml +16 -16
- package/scripts/completion-wakeup.py +390 -0
- package/scripts/configure.sh +4 -4
- package/scripts/dispatch.sh +323 -32
- package/scripts/doctor.sh +55 -1
- package/scripts/jobs.sh +64 -17
- package/scripts/lib/aa_policy.py +473 -0
- package/scripts/lib/aa_retry.py +77 -0
- package/scripts/lib/common.sh +106 -1
- package/scripts/lib/job-worker.sh +314 -20
- package/scripts/lib/live-protocol.sh +147 -2
- package/scripts/lib/native.py +507 -0
- package/scripts/lib/normalize-claude-stream.py +72 -0
- package/scripts/lib/prepare-agy-mode.py +374 -0
- package/scripts/release-audit.sh +103 -0
- package/scripts/runners/run-claude.sh +81 -47
- package/scripts/runners/run-codex-live.py +462 -0
- package/scripts/runners/run-codex.sh +62 -3
- package/scripts/runners/run-gemini.sh +85 -10
- package/scripts/runners/run-grok-live.py +426 -0
- package/scripts/runners/run-grok.sh +117 -6
- package/scripts/runners/run-vote.sh +6 -3
- package/skills/omnilane/SKILL.md +217 -81
package/scripts/jobs.sh
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
# omnilane background-job helper.
|
|
4
|
+
# Native: jobs.sh [--json] complete-native JOB_ID COMPLETION.json
|
|
5
|
+
# Native pending cancellation records state only; the caller owns its agent.
|
|
4
6
|
# Usage: jobs.sh [--json] list | status JOB_ID | result JOB_ID | stats [--last N]
|
|
5
7
|
# jobs.sh [--json] recommend [--last N] [--lane L] [--min-samples N]
|
|
6
8
|
# jobs.sh wait JOB_ID [--timeout N]
|
|
@@ -29,7 +31,7 @@ die() {
|
|
|
29
31
|
exit "$rc"
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|send ID TEXT|watch ID|close ID|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|threads [list] [--json]|threads show NAME [--json]|threads rm NAME|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
34
|
+
USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done|pending|cancelled]|status ID|result ID|complete-native ID FILE|tail ID [--lines N]|send ID TEXT|watch ID|close ID|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|threads [list] [--json]|threads show NAME [--json]|threads rm NAME|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
33
35
|
|
|
34
36
|
usage() {
|
|
35
37
|
die 2 "$USAGE_TEXT"
|
|
@@ -137,11 +139,15 @@ load_job_vendor() {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
require_live_job() {
|
|
142
|
+
local action="${1:-}"
|
|
140
143
|
load_job_vendor
|
|
141
144
|
live_vendor_capable "$JOB_VENDOR" ||
|
|
142
145
|
die 1 "job vendor '$JOB_VENDOR' is not live-capable (live vendors: $(live_capable_vendors)); it runs in single-shot mode"
|
|
143
146
|
[[ "$JOB_SESSION_MODE" == "live" ]] ||
|
|
144
147
|
die 1 "job vendor '$JOB_VENDOR' was dispatched in single-shot mode"
|
|
148
|
+
if [[ "$action" == "send" && "$JOB_VENDOR" == "grok" ]]; then
|
|
149
|
+
echo "note: Grok send cancels an active turn before starting the new prompt; conversation context survives, but in-progress work is lost"
|
|
150
|
+
fi
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
wait_for_live_ready() {
|
|
@@ -194,7 +200,7 @@ parse_stats_metadata() {
|
|
|
194
200
|
parse_audit_metadata() {
|
|
195
201
|
local value="$1" current_re legacy_re
|
|
196
202
|
local json_string='([^"\\]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*'
|
|
197
|
-
current_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"session_mode\":\"(live|single-shot)\",\"idle_timeout\":(0|[1-9][0-9]*),\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work|sysops)\",\"workdir\":\"${json_string}\",\"foreman_session\":\"${json_string}\",\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"
|
|
203
|
+
current_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"session_mode\":\"(live|single-shot)\",\"idle_timeout\":(0|[1-9][0-9]*),\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work|sysops)\",\"workdir\":\"${json_string}\",\"foreman_session\":\"${json_string}\",(\"thread\":\"${json_string}\",\"thread_turn\":[1-9][0-9]*,)?\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"(,\"worker_interpreter_path\":\"${json_string}\",\"worker_interpreter_version\":\"${json_string}\",\"job_worker_sha256\":\"[0-9a-f]{64}\")?\\}$"
|
|
198
204
|
legacy_re="^\\{\"lane\":\"[a-z][a-z0-9-]*\",\"vendor\":\"(${OMNILANE_VENDOR_ALT})\",\"model\":\"${json_string}\",\"effort\":\"${json_string}\",\"timeout\":(0|[1-9][0-9]*),\"job_timeout\":(null|0|[1-9][0-9]*),\"mode\":\"(advise|work|sysops)\",\"workdir\":\"${json_string}\",(\"foreman_session\":\"${json_string}\",)?\"candidate\":\"[1-9][0-9]*/[1-9][0-9]*\",\"started\":\"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\"\\}$"
|
|
199
205
|
[[ "$value" =~ $current_re || "$value" =~ $legacy_re ]]
|
|
200
206
|
}
|
|
@@ -318,6 +324,23 @@ done
|
|
|
318
324
|
# set -u on Bash 3.2; a bare `jobs.sh` must reach usage, not crash.
|
|
319
325
|
set -- ${args[@]+"${args[@]}"}
|
|
320
326
|
COMMAND="${1:-unknown}"
|
|
327
|
+
|
|
328
|
+
# Native jobs have no shell worker PID. Intercept before any CLI lifecycle
|
|
329
|
+
# operation, including cancellation, retry, wait and rm. Python validates the
|
|
330
|
+
# store, job ID, lock and record before accepting or publishing a transition.
|
|
331
|
+
if [[ "$COMMAND" == "complete-native" ]] ||
|
|
332
|
+
{ [[ "${2:-}" =~ $JOB_ID_PATTERN ]] &&
|
|
333
|
+
{ [[ -e "$JOBS/${2}/native.json" || -L "$JOBS/${2}/native.json" ||
|
|
334
|
+
-e "$JOBS/${2}/native.lock" || -L "$JOBS/${2}/native.lock" ]]; }; }; then
|
|
335
|
+
case "$COMMAND" in
|
|
336
|
+
complete-native) [[ $# -eq 3 ]] || usage ;;
|
|
337
|
+
status|result|cancel) [[ $# -eq 2 ]] || usage ;;
|
|
338
|
+
*) die 2 "native job supports status, result, cancel and complete-native; caller owns agent lifecycle" ;;
|
|
339
|
+
esac
|
|
340
|
+
native_args=(job --home "$OMNILANE_HOME")
|
|
341
|
+
[[ "$JSON_MODE" -eq 0 ]] || native_args+=(--json)
|
|
342
|
+
exec python3 "$OMNILANE_REPO/scripts/lib/native.py" "${native_args[@]}" "$@"
|
|
343
|
+
fi
|
|
321
344
|
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
322
345
|
case "$COMMAND" in
|
|
323
346
|
list|status|result|stats|recommend|audit|threads) ;;
|
|
@@ -405,7 +428,7 @@ case "${1:-}" in
|
|
|
405
428
|
send)
|
|
406
429
|
[[ "$JSON_MODE" -eq 0 && $# -eq 3 ]] || usage
|
|
407
430
|
select_job "$2"
|
|
408
|
-
require_live_job
|
|
431
|
+
require_live_job send
|
|
409
432
|
wait_for_live_ready
|
|
410
433
|
if [[ "${FOREMAN_SESSION+x}" == "x" ]]; then
|
|
411
434
|
foreman_value="$FOREMAN_SESSION"
|
|
@@ -507,6 +530,15 @@ case "${1:-}" in
|
|
|
507
530
|
while [[ $# -gt 0 ]]; do
|
|
508
531
|
case "$1" in
|
|
509
532
|
--background) retry_background=1; shift ;;
|
|
533
|
+
--caller-context)
|
|
534
|
+
[[ "$#" -ge 2 ]] || die 2 "retry --caller-context requires a file"
|
|
535
|
+
export OMNILANE_AA_CALLER_CONTEXT="$2"
|
|
536
|
+
unset OMNILANE_AA_OPERATOR_ASSERTED_HUMAN
|
|
537
|
+
shift 2 ;;
|
|
538
|
+
--operator-asserted-human)
|
|
539
|
+
export OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1
|
|
540
|
+
unset OMNILANE_AA_CALLER_CONTEXT
|
|
541
|
+
shift ;;
|
|
510
542
|
*) usage ;;
|
|
511
543
|
esac
|
|
512
544
|
done
|
|
@@ -517,18 +549,14 @@ case "${1:-}" in
|
|
|
517
549
|
read_public_metadata "$JOB_DIR/meta.json" || {
|
|
518
550
|
die 1 "cannot retry: unreadable job metadata"
|
|
519
551
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
retry_lane="${
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
retry_timeout="${BASH_REMATCH[5]}"
|
|
529
|
-
retry_job_timeout="${BASH_REMATCH[6]}"
|
|
530
|
-
retry_mode="${BASH_REMATCH[7]}"
|
|
531
|
-
retry_workdir="${BASH_REMATCH[8]}"
|
|
552
|
+
retry_fields_text="$(python3 "$OMNILANE_REPO/scripts/lib/aa_retry.py" --metadata "$JOB_DIR")" || exit $?
|
|
553
|
+
retry_fields=()
|
|
554
|
+
while IFS= read -r retry_field; do retry_fields+=("$retry_field"); done <<< "$retry_fields_text"
|
|
555
|
+
[[ "${#retry_fields[@]}" -eq 8 ]] || die 1 "invalid retry metadata fields"
|
|
556
|
+
retry_lane="${retry_fields[0]}"; retry_vendor="${retry_fields[1]}"
|
|
557
|
+
retry_model="${retry_fields[2]}"; retry_effort="${retry_fields[3]}"
|
|
558
|
+
retry_timeout="${retry_fields[4]}"; retry_job_timeout="${retry_fields[5]}"
|
|
559
|
+
retry_mode="${retry_fields[6]}"; retry_workdir="${retry_fields[7]}"
|
|
532
560
|
task_path="$JOB_DIR/task.txt"
|
|
533
561
|
[[ -f "$task_path" && ! -L "$task_path" ]] || {
|
|
534
562
|
die 1 "cannot retry: original task text is missing"
|
|
@@ -537,6 +565,13 @@ case "${1:-}" in
|
|
|
537
565
|
die 1 "cannot retry: original workdir no longer exists: $retry_workdir"
|
|
538
566
|
}
|
|
539
567
|
retry_args=(--mode "$retry_mode" --workdir "$retry_workdir" --timeout "$retry_timeout")
|
|
568
|
+
policy_retry_args="$(python3 "$OMNILANE_REPO/scripts/lib/aa_retry.py" "$JOB_DIR")" || exit $?
|
|
569
|
+
while IFS= read -r policy_arg; do
|
|
570
|
+
retry_args+=("$policy_arg")
|
|
571
|
+
done <<< "$policy_retry_args"
|
|
572
|
+
# Retry reauthorizes under the original job, not the current shell identity.
|
|
573
|
+
unset OMNILANE_AA_CALLER_CONTEXT OMNILANE_AA_OPERATOR_ASSERTED_HUMAN OMNILANE_AA_TRANSPORT_OVERLAY OMNILANE_AA_OVERLAY_SHA256
|
|
574
|
+
|
|
540
575
|
[[ "$retry_job_timeout" == "null" ]] || retry_args+=(--job-timeout "$retry_job_timeout")
|
|
541
576
|
# dispatch --vendor only accepts real CLI vendors; an exec gate is re-run
|
|
542
577
|
# through normal lane resolution plus the recorded --model script path.
|
|
@@ -888,8 +923,8 @@ case "${1:-}" in
|
|
|
888
923
|
[[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
|
|
889
924
|
[[ -z "$filter_vendor" ]] || omnilane_known_vendor "$filter_vendor" || die 2 "invalid --vendor value"
|
|
890
925
|
case "$filter_status" in
|
|
891
|
-
""|running|done) ;;
|
|
892
|
-
*) die 2 "invalid --status value (want running or
|
|
926
|
+
""|running|done|pending|cancelled) ;;
|
|
927
|
+
*) die 2 "invalid --status value (want running, done, pending or cancelled)" ;;
|
|
893
928
|
esac
|
|
894
929
|
if [[ ! -d "$JOBS" ]]; then
|
|
895
930
|
[[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":[]}\n'
|
|
@@ -925,6 +960,16 @@ case "${1:-}" in
|
|
|
925
960
|
else
|
|
926
961
|
json_state="running"
|
|
927
962
|
fi
|
|
963
|
+
if [[ -e "$JOBS/$id/native.json" || -L "$JOBS/$id/native.json" || -e "$JOBS/$id/native.lock" ]]; then
|
|
964
|
+
json_state="$(python3 "$OMNILANE_REPO/scripts/lib/native.py" job --home "$OMNILANE_HOME" list-state "$id")" || die 1 "invalid native state"
|
|
965
|
+
state="$json_state"
|
|
966
|
+
# Native completion lives in native.json, not a PID/exit pair.
|
|
967
|
+
exit_json="null"
|
|
968
|
+
if [[ "$json_state" != "pending" ]]; then
|
|
969
|
+
native_status="$(python3 "$OMNILANE_REPO/scripts/lib/native.py" job --home "$OMNILANE_HOME" --json status "$id")" || die 1 "invalid native state"
|
|
970
|
+
exit_json="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["job"]["exit_code"])' "$native_status")"
|
|
971
|
+
fi
|
|
972
|
+
fi
|
|
928
973
|
metadata=""
|
|
929
974
|
metadata_json="null"
|
|
930
975
|
metadata_status="missing"
|
|
@@ -949,6 +994,7 @@ case "${1:-}" in
|
|
|
949
994
|
case "$json_state" in
|
|
950
995
|
done) [[ "$filter_status" == "done" ]] || continue ;;
|
|
951
996
|
running) [[ "$filter_status" == "running" ]] || continue ;;
|
|
997
|
+
pending|cancelled) [[ "$filter_status" == "$json_state" ]] || continue ;;
|
|
952
998
|
*) continue ;;
|
|
953
999
|
esac
|
|
954
1000
|
fi
|
|
@@ -1225,6 +1271,7 @@ case "${1:-}" in
|
|
|
1225
1271
|
[[ -d "$job_dir" && ! -L "$job_dir" ]] || continue
|
|
1226
1272
|
id="${job_dir##*/}"
|
|
1227
1273
|
[[ "$id" =~ $JOB_ID_PATTERN ]] || continue
|
|
1274
|
+
[[ ! -e "$job_dir/native.lock" && ! -e "$job_dir/native.json" ]] || continue
|
|
1228
1275
|
read_exit_code "$job_dir/exit" || continue
|
|
1229
1276
|
completed+=("$id")
|
|
1230
1277
|
done
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Frozen exact-AA downward-delegation decision engine.
|
|
3
|
+
|
|
4
|
+
This module is intentionally provider-free. It validates one explicit caller
|
|
5
|
+
assertion against the frozen registry, resolves only runtime-verified target
|
|
6
|
+
mappings, and emits a structured allow/deny decision. The JSON context is
|
|
7
|
+
cooperative workflow metadata, not operating-system authentication.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import re
|
|
18
|
+
import stat
|
|
19
|
+
import tempfile
|
|
20
|
+
import socket
|
|
21
|
+
import sys
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MAX_BYTES = 1_048_576
|
|
26
|
+
# Approval anchor for the frozen AA v4.2 2026-09-07 source bytes. Updating this
|
|
27
|
+
# constant is a governance change, never a caller argument/environment override.
|
|
28
|
+
APPROVED_REGISTRY_SHA256 = "0782c87de123c02738c3ff60e4bc3c1cc10d110113e872b8f8627212861cdaab"
|
|
29
|
+
|
|
30
|
+
IDENTITY_FIELDS = ("vendor", "model", "effort", "reasoning", "fallback")
|
|
31
|
+
IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}\Z")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PolicyError(ValueError):
|
|
35
|
+
"""A public, non-sensitive policy validation error."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _check(condition: bool, message: str) -> None:
|
|
39
|
+
if not condition:
|
|
40
|
+
raise PolicyError(message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
44
|
+
result: dict[str, Any] = {}
|
|
45
|
+
for key, value in pairs:
|
|
46
|
+
_check(key not in result, "duplicate JSON key")
|
|
47
|
+
result[key] = value
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _read_bytes(path: str | Path) -> bytes:
|
|
52
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
53
|
+
with os.fdopen(fd, "rb") as stream:
|
|
54
|
+
info = os.fstat(stream.fileno())
|
|
55
|
+
_check(stat.S_ISREG(info.st_mode), "policy input must be a regular file")
|
|
56
|
+
_check(info.st_size <= MAX_BYTES, "policy input is too large")
|
|
57
|
+
data = stream.read(MAX_BYTES + 1)
|
|
58
|
+
_check(len(data) <= MAX_BYTES, "policy input is too large")
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_json(path: str | Path) -> tuple[dict[str, Any], str]:
|
|
63
|
+
data = _read_bytes(path)
|
|
64
|
+
try:
|
|
65
|
+
value = json.loads(
|
|
66
|
+
data.decode("utf-8"),
|
|
67
|
+
object_pairs_hook=_unique_object,
|
|
68
|
+
parse_constant=lambda _: (_ for _ in ()).throw(PolicyError("invalid JSON number")),
|
|
69
|
+
)
|
|
70
|
+
except (UnicodeError, json.JSONDecodeError) as error:
|
|
71
|
+
raise PolicyError("invalid JSON input") from error
|
|
72
|
+
_check(isinstance(value, dict), "policy input must be a JSON object")
|
|
73
|
+
return value, hashlib.sha256(data).hexdigest()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _exact_fields(value: dict[str, Any], required: set[str]) -> None:
|
|
77
|
+
_check(set(value) == required, "missing or unknown caller-context fields")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _identity(value: Any, label: str) -> dict[str, Any]:
|
|
81
|
+
_check(isinstance(value, dict), f"{label} must be a JSON object")
|
|
82
|
+
_exact_fields(value, set(IDENTITY_FIELDS))
|
|
83
|
+
for key in ("vendor", "model", "reasoning"):
|
|
84
|
+
field = value[key]
|
|
85
|
+
_check(isinstance(field, str) and bool(IDENTIFIER.fullmatch(field)),
|
|
86
|
+
f"invalid {label}.{key}")
|
|
87
|
+
for key in ("effort", "fallback"):
|
|
88
|
+
field = value[key]
|
|
89
|
+
_check(field is None or (isinstance(field, str) and bool(IDENTIFIER.fullmatch(field))),
|
|
90
|
+
f"invalid {label}.{key}")
|
|
91
|
+
return {key: value[key] for key in IDENTITY_FIELDS}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_registry(value: dict[str, Any]) -> dict[str, Any]:
|
|
95
|
+
required = {
|
|
96
|
+
"schema_version", "snapshot", "policy", "scored_configs",
|
|
97
|
+
"unknown_configs", "reference_configs", "aliases", "coverage",
|
|
98
|
+
}
|
|
99
|
+
_check(required <= set(value) <= required | {"schema_notes"},
|
|
100
|
+
"unsupported AA registry schema")
|
|
101
|
+
_check(type(value["schema_version"]) is int and value["schema_version"] == 1,
|
|
102
|
+
"unsupported AA registry version")
|
|
103
|
+
snapshot = value["snapshot"]
|
|
104
|
+
_check(isinstance(snapshot, dict), "invalid AA registry snapshot")
|
|
105
|
+
for key in ("id", "benchmark_version", "as_of", "frozen"):
|
|
106
|
+
_check(key in snapshot, "incomplete AA registry snapshot")
|
|
107
|
+
_check(snapshot["benchmark_version"] == "4.2"
|
|
108
|
+
and snapshot["as_of"] == "2026-09-07"
|
|
109
|
+
and snapshot["frozen"] is True,
|
|
110
|
+
"AA registry is not frozen v4.2 dated 2026-09-07")
|
|
111
|
+
policy = value["policy"]
|
|
112
|
+
_check(isinstance(policy, dict)
|
|
113
|
+
and policy.get("decision") == "target_score <= min(caller_score, inherited_ceiling)"
|
|
114
|
+
and policy.get("same_score_allowed") is True
|
|
115
|
+
and policy.get("unknown") == "deny"
|
|
116
|
+
and policy.get("transport_mapping_requires_runtime_verification") is True,
|
|
117
|
+
"AA registry policy contract mismatch")
|
|
118
|
+
rows = value["scored_configs"]
|
|
119
|
+
_check(isinstance(rows, list) and rows, "AA registry has no scored configurations")
|
|
120
|
+
seen: set[str] = set()
|
|
121
|
+
for row in rows:
|
|
122
|
+
_check(isinstance(row, dict), "invalid AA scored configuration")
|
|
123
|
+
for key in (*IDENTITY_FIELDS, "id", "score", "estimated", "benchmark_version",
|
|
124
|
+
"as_of", "transport_mapping"):
|
|
125
|
+
_check(key in row, "incomplete AA scored configuration")
|
|
126
|
+
_check(isinstance(row["id"], str) and row["id"] not in seen,
|
|
127
|
+
"duplicate AA scored configuration")
|
|
128
|
+
seen.add(row["id"])
|
|
129
|
+
_identity({key: row[key] for key in IDENTITY_FIELDS}, "registry identity")
|
|
130
|
+
_check(type(row["score"]) is int and 0 <= row["score"] <= 100,
|
|
131
|
+
"invalid AA score")
|
|
132
|
+
_check(type(row["estimated"]) is bool, "invalid AA estimated flag")
|
|
133
|
+
_check(row["benchmark_version"] == "4.2" and row["as_of"] == "2026-09-07",
|
|
134
|
+
"mixed AA registry snapshot")
|
|
135
|
+
_check(isinstance(row["transport_mapping"], dict), "invalid transport mapping")
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def apply_transport_overlay(registry: dict[str, Any]) -> None:
|
|
140
|
+
"""Host-local request selector proof, independent of frozen AA scores."""
|
|
141
|
+
path = os.environ.get("OMNILANE_AA_TRANSPORT_OVERLAY")
|
|
142
|
+
if not path:
|
|
143
|
+
return
|
|
144
|
+
overlay, digest = _read_json(path)
|
|
145
|
+
expected = os.environ.get("OMNILANE_AA_OVERLAY_SHA256")
|
|
146
|
+
_check(not expected or expected == digest, "transport overlay changed after initial decision")
|
|
147
|
+
_check(overlay.get("schema_version") == 1, "unsupported transport overlay")
|
|
148
|
+
_check(overlay.get("snapshot_id") == registry["snapshot"]["id"], "transport overlay snapshot mismatch")
|
|
149
|
+
_check(overlay.get("host") == socket.gethostname(), "transport overlay host mismatch")
|
|
150
|
+
for evidence in overlay.get("evidence", []):
|
|
151
|
+
fd = os.open(evidence["path"], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
152
|
+
with os.fdopen(fd, "rb") as stream:
|
|
153
|
+
_check(stat.S_ISREG(os.fstat(stream.fileno()).st_mode), "invalid transport evidence file")
|
|
154
|
+
digest_file = hashlib.sha256()
|
|
155
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
156
|
+
digest_file.update(block)
|
|
157
|
+
_check(digest_file.hexdigest() == evidence["sha256"], "transport contract evidence changed")
|
|
158
|
+
_check(bool(overlay.get("evidence")), "transport overlay requires local evidence")
|
|
159
|
+
for mapping in overlay.get("mappings", []):
|
|
160
|
+
rows = [row for row in registry["scored_configs"] if row["id"] == mapping.get("config_id")]
|
|
161
|
+
_check(len(rows) == 1, "unknown overlay config")
|
|
162
|
+
row = rows[0]
|
|
163
|
+
_check(mapping.get("identity") == _row_identity(row), "overlay exact identity mismatch")
|
|
164
|
+
_check(mapping.get("verification") == "request-selector-contract", "unsupported overlay verification")
|
|
165
|
+
_check(mapping.get("runtime_effort") == row["effort"], "overlay effort mismatch")
|
|
166
|
+
selector_type = mapping.get("selector_type", "model_and_effort")
|
|
167
|
+
_check(selector_type in ("model_and_effort", "model_id_encoded_effort"), "unknown selector type")
|
|
168
|
+
if selector_type == "model_id_encoded_effort":
|
|
169
|
+
_check(row["vendor"] == "gemini", "unsupported encoded-effort vendor")
|
|
170
|
+
_check(mapping.get("runtime_model") in row["transport_mapping"].get("candidate_model_ids", []), "unproven encoded model selector")
|
|
171
|
+
_check(mapping["runtime_model"].endswith("-" + row["effort"]), "encoded effort does not match exact tuple")
|
|
172
|
+
else:
|
|
173
|
+
_check(mapping.get("runtime_model") == row["model"], "overlay model mismatch")
|
|
174
|
+
row["transport_mapping"].update(
|
|
175
|
+
status="verified", runtime_verified=True,
|
|
176
|
+
runtime_model=mapping["runtime_model"], runtime_effort=mapping["runtime_effort"],
|
|
177
|
+
selector_type=selector_type,
|
|
178
|
+
verification="request-selector-contract", upstream_identity_verified=False,
|
|
179
|
+
overlay_sha256=digest, overlay_host=overlay["host"],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def load_registry(path: str | Path, expected_sha256: str | None = None) -> tuple[dict[str, Any], str]:
|
|
184
|
+
value, digest = _read_json(path)
|
|
185
|
+
_check(digest == APPROVED_REGISTRY_SHA256, "unapproved AA registry; use the approved frozen snapshot byte-for-byte")
|
|
186
|
+
if expected_sha256:
|
|
187
|
+
_check(digest == expected_sha256, "AA registry changed after initial decision")
|
|
188
|
+
registry = _validate_registry(value)
|
|
189
|
+
apply_transport_overlay(registry)
|
|
190
|
+
return registry, digest
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def load_caller(path: str | Path, registry: dict[str, Any],
|
|
194
|
+
expected_sha256: str | None = None) -> tuple[dict[str, Any], str]:
|
|
195
|
+
value, digest = _read_json(path)
|
|
196
|
+
if expected_sha256:
|
|
197
|
+
_check(digest == expected_sha256, "caller context changed after initial decision")
|
|
198
|
+
_check(type(value.get("schema_version")) is int and value["schema_version"] == 1,
|
|
199
|
+
"unsupported caller-context version")
|
|
200
|
+
_check(value.get("snapshot_id") == registry["snapshot"]["id"],
|
|
201
|
+
"caller-context snapshot does not match frozen registry")
|
|
202
|
+
_check(value.get("kind") == "model", "unsupported caller-context kind")
|
|
203
|
+
_exact_fields(value, {"schema_version", "snapshot_id", "kind", "caller", "inherited_ceiling"})
|
|
204
|
+
value["caller"] = _identity(value["caller"], "caller")
|
|
205
|
+
ceiling = value["inherited_ceiling"]
|
|
206
|
+
_check(type(ceiling) is int and 0 <= ceiling <= 100,
|
|
207
|
+
"invalid inherited caller ceiling")
|
|
208
|
+
return value, digest
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _row_identity(row: dict[str, Any]) -> dict[str, Any]:
|
|
212
|
+
return {key: row[key] for key in IDENTITY_FIELDS}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _matching_rows(registry: dict[str, Any], identity: dict[str, Any]) -> list[dict[str, Any]]:
|
|
216
|
+
return [row for row in registry["scored_configs"] if _row_identity(row) == identity]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _unknown_reason(registry: dict[str, Any], identity: dict[str, Any]) -> str | None:
|
|
220
|
+
for row in registry["unknown_configs"]:
|
|
221
|
+
if all(row.get(key) == identity[key] for key in IDENTITY_FIELDS):
|
|
222
|
+
return row.get("reason")
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _runtime_target(registry: dict[str, Any], vendor: str, model: str,
|
|
227
|
+
effort: str | None, target_config: str | None) -> tuple[dict[str, Any] | None, str, dict[str, Any]]:
|
|
228
|
+
exact_id = [row for row in registry["scored_configs"] if row["id"] == target_config] if target_config else registry["scored_configs"]
|
|
229
|
+
if target_config and not exact_id:
|
|
230
|
+
return None, "unknown-target-config", {"target_config": target_config}
|
|
231
|
+
vendor_rows = [row for row in exact_id if row["vendor"] == vendor]
|
|
232
|
+
candidates: list[dict[str, Any]] = []
|
|
233
|
+
unresolved: list[str] = []
|
|
234
|
+
for row in vendor_rows:
|
|
235
|
+
mapping = row["transport_mapping"]
|
|
236
|
+
model_ids = mapping.get("candidate_model_ids", [])
|
|
237
|
+
runtime_model = mapping.get("runtime_model")
|
|
238
|
+
runtime_effort = mapping.get("runtime_effort", row["effort"])
|
|
239
|
+
model_match = model == runtime_model if runtime_model is not None else model in model_ids
|
|
240
|
+
encoded_effort = mapping.get("selector_type") == "model_id_encoded_effort"
|
|
241
|
+
if not model_match:
|
|
242
|
+
continue
|
|
243
|
+
if encoded_effort and effort not in (None, runtime_effort):
|
|
244
|
+
return None, "encoded-effort-conflict", {"model": model, "effort": effort, "encoded_effort": runtime_effort}
|
|
245
|
+
if not encoded_effort and effort != runtime_effort:
|
|
246
|
+
continue
|
|
247
|
+
if mapping.get("runtime_verified") is True and mapping.get("status") in ("verified", "resolved"):
|
|
248
|
+
candidates.append(row)
|
|
249
|
+
else:
|
|
250
|
+
unresolved.append(row["id"])
|
|
251
|
+
if vendor == "grok" and candidates:
|
|
252
|
+
# The checked-in Grok runner accepts EFFORT for interface parity but
|
|
253
|
+
# discards it. A scored reasoning/effort row therefore cannot be
|
|
254
|
+
# proven by that runtime surface.
|
|
255
|
+
return None, "runtime-effort-discarded", {"vendor": vendor, "model": model, "effort": effort}
|
|
256
|
+
if len(candidates) == 1:
|
|
257
|
+
return candidates[0], "runtime-mapping-verified", {}
|
|
258
|
+
if len(candidates) > 1:
|
|
259
|
+
return None, "ambiguous-runtime-mapping", {"candidate_config_ids": [row["id"] for row in candidates]}
|
|
260
|
+
if unresolved:
|
|
261
|
+
return None, "runtime-mapping-unverified", {"candidate_config_ids": unresolved}
|
|
262
|
+
return None, "unknown-target-runtime", {"vendor": vendor, "model": model, "effort": effort}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def decide(registry: dict[str, Any], registry_sha256: str, *,
|
|
266
|
+
vendor: str, model: str, effort: str | None,
|
|
267
|
+
caller: dict[str, Any] | None, caller_sha256: str | None,
|
|
268
|
+
operator_asserted_human: bool = False,
|
|
269
|
+
target_config: str | None = None) -> dict[str, Any]:
|
|
270
|
+
base: dict[str, Any] = {
|
|
271
|
+
"schema_version": 1,
|
|
272
|
+
"snapshot_id": registry["snapshot"]["id"],
|
|
273
|
+
"registry_sha256": registry_sha256,
|
|
274
|
+
"allowed": False,
|
|
275
|
+
"code": "deny",
|
|
276
|
+
"message": "AA policy denied dispatch",
|
|
277
|
+
"caller_kind": "operator-asserted-human" if operator_asserted_human else "model",
|
|
278
|
+
"target_request": {"vendor": vendor, "model": model, "effort": effort},
|
|
279
|
+
"target_config_id": target_config,
|
|
280
|
+
"caller_score": None,
|
|
281
|
+
"inherited_ceiling": None,
|
|
282
|
+
"effective_ceiling": None,
|
|
283
|
+
"target_score": None,
|
|
284
|
+
"target_estimated": None,
|
|
285
|
+
"child_context": None,
|
|
286
|
+
"evidence_limit": "workflow metadata and selected model arguments do not prove actual provider identity",
|
|
287
|
+
}
|
|
288
|
+
if operator_asserted_human:
|
|
289
|
+
base.update(
|
|
290
|
+
allowed=True,
|
|
291
|
+
code="operator-asserted-human-exemption",
|
|
292
|
+
message="explicit operator assertion bypassed model-level AA ceiling",
|
|
293
|
+
evidence_limit="operator assertion is cooperative metadata, not authentication or provider-identity proof",
|
|
294
|
+
)
|
|
295
|
+
return base
|
|
296
|
+
if caller is None:
|
|
297
|
+
base.update(
|
|
298
|
+
code="missing-caller-context",
|
|
299
|
+
message="provide --caller-context FILE or explicitly assert --operator-asserted-human",
|
|
300
|
+
)
|
|
301
|
+
return base
|
|
302
|
+
base["caller_context_sha256"] = caller_sha256
|
|
303
|
+
caller_identity = caller["caller"]
|
|
304
|
+
caller_rows = _matching_rows(registry, caller_identity)
|
|
305
|
+
if len(caller_rows) != 1:
|
|
306
|
+
detail = _unknown_reason(registry, caller_identity)
|
|
307
|
+
base.update(
|
|
308
|
+
code="unknown-caller-config" if not caller_rows else "ambiguous-caller-config",
|
|
309
|
+
message=detail or "caller exact vendor/model/effort/reasoning/fallback is not uniquely scored",
|
|
310
|
+
caller=caller_identity,
|
|
311
|
+
)
|
|
312
|
+
return base
|
|
313
|
+
caller_row = caller_rows[0]
|
|
314
|
+
effective = min(caller_row["score"], caller["inherited_ceiling"])
|
|
315
|
+
base.update(
|
|
316
|
+
caller=caller_identity,
|
|
317
|
+
caller_score=caller_row["score"],
|
|
318
|
+
inherited_ceiling=caller["inherited_ceiling"],
|
|
319
|
+
effective_ceiling=effective,
|
|
320
|
+
)
|
|
321
|
+
target_row, mapping_code, detail = _runtime_target(
|
|
322
|
+
registry, vendor, model, effort, target_config
|
|
323
|
+
)
|
|
324
|
+
if target_row is None:
|
|
325
|
+
base.update(code=mapping_code, message="target runtime cannot be mapped to one verified exact AA configuration", **detail)
|
|
326
|
+
return base
|
|
327
|
+
target_score = target_row["score"]
|
|
328
|
+
target_identity = _row_identity(target_row)
|
|
329
|
+
base.update(
|
|
330
|
+
target_config_id=target_row["id"],
|
|
331
|
+
target=target_identity,
|
|
332
|
+
target_score=target_score,
|
|
333
|
+
target_estimated=target_row["estimated"],
|
|
334
|
+
)
|
|
335
|
+
if target_score > effective:
|
|
336
|
+
base.update(
|
|
337
|
+
code="target-above-effective-ceiling",
|
|
338
|
+
message=f"target score {target_score} exceeds effective caller ceiling {effective}",
|
|
339
|
+
)
|
|
340
|
+
return base
|
|
341
|
+
child_context = {
|
|
342
|
+
"schema_version": 1,
|
|
343
|
+
"snapshot_id": registry["snapshot"]["id"],
|
|
344
|
+
"kind": "model",
|
|
345
|
+
"caller": target_identity,
|
|
346
|
+
"inherited_ceiling": effective,
|
|
347
|
+
}
|
|
348
|
+
base.update(
|
|
349
|
+
allowed=True,
|
|
350
|
+
code="same-score-allowed" if target_score == effective else "downward-allowed",
|
|
351
|
+
message="target exact AA score is at or below effective caller ceiling",
|
|
352
|
+
child_context=child_context,
|
|
353
|
+
)
|
|
354
|
+
return base
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _normalized_effort(value: str) -> str | None:
|
|
358
|
+
return None if value in ("", "-") else value
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _json_line(value: dict[str, Any]) -> str:
|
|
362
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def atomic_bytes(path: Path, content: bytes) -> None:
|
|
366
|
+
"""Publish private metadata without exposing a partial file."""
|
|
367
|
+
fd, name = tempfile.mkstemp(prefix=".aa-", dir=path.parent)
|
|
368
|
+
try:
|
|
369
|
+
with os.fdopen(fd, "wb") as stream:
|
|
370
|
+
stream.write(content)
|
|
371
|
+
stream.flush()
|
|
372
|
+
os.fsync(stream.fileno())
|
|
373
|
+
os.replace(name, path)
|
|
374
|
+
finally:
|
|
375
|
+
if os.path.exists(name):
|
|
376
|
+
os.unlink(name)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def atomic_json(path: Path, value: dict[str, Any]) -> None:
|
|
380
|
+
atomic_bytes(path, _json_line(value).encode("utf-8"))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def publish_context(directory: str | Path, registry: dict[str, Any],
|
|
384
|
+
caller: dict[str, Any] | None, decision: dict[str, Any],
|
|
385
|
+
registry_source: str | Path | None = None) -> dict[str, Any]:
|
|
386
|
+
"""Keep immutable-by-convention authorizer and a distinct narrowed child identity.
|
|
387
|
+
|
|
388
|
+
This is lineage metadata, not authentication. The frozen registry content is
|
|
389
|
+
copied so later retries cannot silently select a different score snapshot.
|
|
390
|
+
"""
|
|
391
|
+
root = Path(directory)
|
|
392
|
+
_check(root.is_dir() and not root.is_symlink(), "invalid context publication directory")
|
|
393
|
+
_check(decision.get("allowed") is True, "cannot publish a denied decision")
|
|
394
|
+
source = Path(registry_source) if registry_source else Path(__file__).resolve().parents[2] / "config/aa-model-policy.json"
|
|
395
|
+
source_bytes = _read_bytes(source)
|
|
396
|
+
_check(hashlib.sha256(source_bytes).hexdigest() == APPROVED_REGISTRY_SHA256, "unapproved AA registry snapshot publication")
|
|
397
|
+
atomic_bytes(root / "aa-registry.json", source_bytes)
|
|
398
|
+
registry_sha = hashlib.sha256((root / "aa-registry.json").read_bytes()).hexdigest()
|
|
399
|
+
if caller is not None:
|
|
400
|
+
atomic_json(root / "aa-authorizer.json", caller)
|
|
401
|
+
child = decision.get("child_context")
|
|
402
|
+
if child is not None:
|
|
403
|
+
atomic_json(root / "aa-child-context.json", child)
|
|
404
|
+
lineage = {
|
|
405
|
+
"schema_version": 1,
|
|
406
|
+
"registry_sha256": registry_sha,
|
|
407
|
+
"authorizer_sha256": hashlib.sha256((root / "aa-authorizer.json").read_bytes()).hexdigest() if caller else None,
|
|
408
|
+
"operator_asserted_human": decision["caller_kind"] == "operator-asserted-human",
|
|
409
|
+
"target_config_id": decision["target_config_id"],
|
|
410
|
+
"target_request": decision["target_request"],
|
|
411
|
+
"effective_ceiling": decision["effective_ceiling"],
|
|
412
|
+
"child_context": child,
|
|
413
|
+
}
|
|
414
|
+
overlay_path = os.environ.get("OMNILANE_AA_TRANSPORT_OVERLAY")
|
|
415
|
+
if overlay_path:
|
|
416
|
+
overlay, _ = _read_json(overlay_path)
|
|
417
|
+
atomic_json(root / "aa-transport-overlay.json", overlay)
|
|
418
|
+
lineage["transport_overlay_sha256"] = hashlib.sha256((root / "aa-transport-overlay.json").read_bytes()).hexdigest()
|
|
419
|
+
atomic_json(root / "aa-lineage.json", lineage)
|
|
420
|
+
atomic_json(root / "aa-decision.json", decision)
|
|
421
|
+
return lineage
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def main(argv: list[str] | None = None) -> int:
|
|
425
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
426
|
+
parser.add_argument("--registry", required=True)
|
|
427
|
+
parser.add_argument("--expected-registry-sha256")
|
|
428
|
+
source = parser.add_mutually_exclusive_group()
|
|
429
|
+
source.add_argument("--caller-context")
|
|
430
|
+
source.add_argument("--operator-asserted-human", action="store_true")
|
|
431
|
+
parser.add_argument("--expected-caller-sha256")
|
|
432
|
+
parser.add_argument("--vendor", required=True)
|
|
433
|
+
parser.add_argument("--model", required=True)
|
|
434
|
+
parser.add_argument("--effort", required=True)
|
|
435
|
+
parser.add_argument("--target-config")
|
|
436
|
+
parser.add_argument("--publish-dir")
|
|
437
|
+
args = parser.parse_args(argv)
|
|
438
|
+
try:
|
|
439
|
+
registry, registry_sha = load_registry(args.registry, args.expected_registry_sha256)
|
|
440
|
+
caller = None
|
|
441
|
+
caller_sha = None
|
|
442
|
+
if args.caller_context:
|
|
443
|
+
caller, caller_sha = load_caller(
|
|
444
|
+
args.caller_context, registry, args.expected_caller_sha256
|
|
445
|
+
)
|
|
446
|
+
result = decide(
|
|
447
|
+
registry,
|
|
448
|
+
registry_sha,
|
|
449
|
+
vendor=args.vendor,
|
|
450
|
+
model=args.model,
|
|
451
|
+
effort=_normalized_effort(args.effort),
|
|
452
|
+
caller=caller,
|
|
453
|
+
caller_sha256=caller_sha,
|
|
454
|
+
operator_asserted_human=args.operator_asserted_human,
|
|
455
|
+
target_config=args.target_config,
|
|
456
|
+
)
|
|
457
|
+
if args.publish_dir and result["allowed"]:
|
|
458
|
+
publish_context(args.publish_dir, registry, caller, result, args.registry)
|
|
459
|
+
print(_json_line(result), end="")
|
|
460
|
+
return 0 if result["allowed"] else 3
|
|
461
|
+
except (PolicyError, OSError, RecursionError, TypeError, KeyError) as error:
|
|
462
|
+
message = str(error) if isinstance(error, PolicyError) else "invalid or inaccessible AA policy input"
|
|
463
|
+
print(_json_line({
|
|
464
|
+
"schema_version": 1,
|
|
465
|
+
"allowed": False,
|
|
466
|
+
"code": "invalid-policy-input",
|
|
467
|
+
"message": message,
|
|
468
|
+
}), end="")
|
|
469
|
+
return 2
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
if __name__ == "__main__":
|
|
473
|
+
sys.exit(main())
|