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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +71 -1
  4. package/README.ja.md +63 -33
  5. package/README.ko.md +63 -32
  6. package/README.md +147 -86
  7. package/README.zh-CN.md +61 -30
  8. package/README.zh-TW.md +124 -75
  9. package/VERSION +1 -1
  10. package/config/aa-model-policy.json +3046 -0
  11. package/docs/aa-model-coverage-2026-09-05.json +29204 -0
  12. package/docs/completion-wakeup.md +126 -0
  13. package/docs/model-capabilities-2026-09.md +380 -0
  14. package/docs/native-executor.md +264 -0
  15. package/docs/release-notes-0.42.1.md +32 -0
  16. package/hooks/routing-instruction.md +101 -40
  17. package/package.json +8 -2
  18. package/plugin.json +2 -2
  19. package/routing.local.yaml.example +8 -3
  20. package/routing.yaml +16 -16
  21. package/scripts/completion-wakeup.py +390 -0
  22. package/scripts/configure.sh +4 -4
  23. package/scripts/dispatch.sh +323 -32
  24. package/scripts/doctor.sh +55 -1
  25. package/scripts/jobs.sh +64 -17
  26. package/scripts/lib/aa_policy.py +473 -0
  27. package/scripts/lib/aa_retry.py +77 -0
  28. package/scripts/lib/common.sh +106 -1
  29. package/scripts/lib/job-worker.sh +314 -20
  30. package/scripts/lib/live-protocol.sh +147 -2
  31. package/scripts/lib/native.py +507 -0
  32. package/scripts/lib/normalize-claude-stream.py +72 -0
  33. package/scripts/lib/prepare-agy-mode.py +374 -0
  34. package/scripts/release-audit.sh +103 -0
  35. package/scripts/runners/run-claude.sh +81 -47
  36. package/scripts/runners/run-codex-live.py +462 -0
  37. package/scripts/runners/run-codex.sh +62 -3
  38. package/scripts/runners/run-gemini.sh +85 -10
  39. package/scripts/runners/run-grok-live.py +426 -0
  40. package/scripts/runners/run-grok.sh +117 -6
  41. package/scripts/runners/run-vote.sh +6 -3
  42. package/skills/omnilane/SKILL.md +217 -81
@@ -35,6 +35,13 @@ MODE="advise"; WORKDIR="$PWD"; BACKGROUND=0; DRY_RUN=0
35
35
  OVERRIDE_VENDOR=""; OVERRIDE_MODEL=""; OVERRIDE_EFFORT=""; OVERRIDE_TIMEOUT=""
36
36
  OVERRIDE_JOB_TIMEOUT=""; OVERRIDE_IDLE_TIMEOUT=""; SESSION_REQUEST="auto"
37
37
  THREAD_NAME=""; THREAD_MODE=""; THREAD_ID=""; THREAD_TURN=""; THREAD_CREATED=""
38
+ EXECUTOR="auto"; NATIVE_CONTEXT=""; RESOLVE_WITH_CONTEXT=0
39
+ SELECTED_EXECUTOR="cli"; EXECUTOR_REASON="no-native-context"
40
+ AA_POLICY_FILE="${OMNILANE_AA_POLICY_FILE:-$OMNILANE_REPO/config/aa-model-policy.json}"
41
+ AA_CALLER_CONTEXT="${OMNILANE_AA_CALLER_CONTEXT:-}"
42
+ AA_OPERATOR_ASSERTED_HUMAN="${OMNILANE_AA_OPERATOR_ASSERTED_HUMAN:-0}"
43
+ AA_TARGET_CONFIG=""; AA_POLICY_ACTIVE=0; AA_EXPLICIT_TARGET=0
44
+ AA_LAST_DECISION=""; AA_SELECTED_DECISION=""; AA_VOTE_PREFLIGHT=0
38
45
 
39
46
  usage_error() {
40
47
  echo 'usage: dispatch.sh [--background] [--dry-run] [--thread NAME] [flags] LANE "TASK" | [--json] --list|--validate [--json] | [--json] --explain LANE [--json] | --help' >&2
@@ -58,11 +65,18 @@ flags:
58
65
  --background run in the background and print the JOB_ID
59
66
  --dry-run print the fully resolved dispatch plan and stop
60
67
  before any provider call or job state
68
+ --executor auto|native|cli caller-owned native handoff or legacy CLI
69
+ --native-context FILE explicit JSON capabilities; native is not a binary
70
+ --caller-context FILE exact model caller identity plus inherited ceiling
71
+ --operator-asserted-human explicit AA model-ceiling exemption; assertion only
72
+ --aa-policy FILE frozen AA policy registry (default: repo config)
73
+ --transport-overlay FILE host-local hashed request-selector contracts
74
+ --target-config ID pin one runtime-verified registry configuration
61
75
  --mode advise|work|sysops
62
76
  advise (read-only, default), work (may edit files),
63
- or sysops (work without the vendor sandbox, for
64
- service operations like launchctl — codex only;
65
- other vendors treat it as work)
77
+ or sysops (vendor sandbox disabled, for service
78
+ operations like launchctl; supported by
79
+ codex, claude, grok, and gemini)
66
80
  --workdir DIR working directory handed to the vendor CLI
67
81
  --vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|deepseek|zai|mistral|groq|cerebras)
68
82
  --model M override the routed model
@@ -282,6 +296,9 @@ print_dry_run_plan() {
282
296
  print_dry_run_value vendor "$VENDOR"
283
297
  print_dry_run_value model "$MODEL"
284
298
  print_dry_run_value effort "$EFFORT"
299
+ print_dry_run_value executor "$SELECTED_EXECUTOR"
300
+ print_dry_run_value executor_reason "$EXECUTOR_REASON"
301
+ printf 'aa_policy=%s\n' "$AA_SELECTED_DECISION"
285
302
  print_dry_run_value mode "$MODE"
286
303
  print_dry_run_value workdir "$WORKDIR"
287
304
  printf 'timeout=%s\n' "$TIMEOUT"
@@ -351,10 +368,59 @@ routing_candidate_available() {
351
368
  fi
352
369
  }
353
370
 
371
+ aa_policy_decide() {
372
+ # vendor model effort [target-config] -> structured JSON in AA_LAST_DECISION
373
+ local vendor="$1" model="$2" effort="$3" target_config="${4:-}" rc=0
374
+ local args=(--registry "$AA_POLICY_FILE" --vendor "$vendor" --model "$model" --effort "$effort")
375
+ [[ -z "$target_config" ]] || args+=(--target-config "$target_config")
376
+ if [[ "$AA_OPERATOR_ASSERTED_HUMAN" == "1" ]]; then
377
+ args+=(--operator-asserted-human)
378
+ elif [[ -n "$AA_CALLER_CONTEXT" ]]; then
379
+ args+=(--caller-context "$AA_CALLER_CONTEXT")
380
+ fi
381
+ AA_LAST_DECISION="$(python3 "$OMNILANE_REPO/scripts/lib/aa_policy.py" "${args[@]}")" || rc=$?
382
+ return "$rc"
383
+ }
384
+
385
+ aa_vote_child_spec() {
386
+ case "$1" in
387
+ codex) printf 'gpt-6-astra\txhigh\n' ;;
388
+ claude) printf 'claude-fable-5-1\txhigh\n' ;;
389
+ gemini) printf 'gemini-3.8-flash-medium\t-\n' ;;
390
+ grok) printf 'grok-4.6\t-\n' ;;
391
+ *) return 1 ;;
392
+ esac
393
+ }
394
+
395
+ aa_policy_candidate_allowed() {
396
+ local vendor="$1" model="$2" effort="$3" voter spec child_model child_effort
397
+ local voters=()
398
+ AA_VOTE_PREFLIGHT=0
399
+ if [[ "$vendor" != "vote" ]]; then
400
+ aa_policy_decide "$vendor" "$model" "$effort" "$AA_TARGET_CONFIG"
401
+ return $?
402
+ fi
403
+ [[ -z "$AA_TARGET_CONFIG" ]] || {
404
+ AA_LAST_DECISION='{"schema_version":1,"allowed":false,"code":"vote-target-config-invalid","message":"--target-config cannot identify an entire vote panel"}'
405
+ return 3
406
+ }
407
+ IFS=',' read -ra voters <<< "$model"
408
+ for voter in ${voters[@]+"${voters[@]}"}; do
409
+ spec="$(aa_vote_child_spec "$voter")" || {
410
+ AA_LAST_DECISION='{"schema_version":1,"allowed":false,"code":"unknown-vote-child","message":"vote panel contains an unknown child"}'
411
+ return 3
412
+ }
413
+ child_model="${spec%%$'\t'*}"; child_effort="${spec##*$'\t'}"
414
+ aa_policy_decide "$voter" "$child_model" "$child_effort" || return $?
415
+ done
416
+ AA_VOTE_PREFLIGHT=1
417
+ return 0
418
+ }
419
+
354
420
  # Pick the first candidate whose vendor CLI is present ("off" always matches).
355
421
  # Sets RESOLVED_SPEC / RESOLVED_FIELDS / RESOLVED_IDX / RESOLVED_TOTAL.
356
422
  resolve_chain() {
357
- local chain="$1" requested_vendor="${2:-}" seg i=0 vendor
423
+ local chain="$1" requested_vendor="${2:-}" seg i=0 vendor model effort considered=0
358
424
  RESOLVED_SPEC=""; RESOLVED_IDX=0; RESOLVED_TOTAL=0; RESOLVED_FIELDS=()
359
425
  local SEGS=() F=()
360
426
  IFS='|' read -ra SEGS <<< "$chain"
@@ -371,16 +437,35 @@ resolve_chain() {
371
437
  }
372
438
  F=("${PARSED_FIELDS[@]}")
373
439
  vendor="${F[0]:-}"
440
+ model="${OVERRIDE_MODEL:-${F[1]:-}}"
441
+ effort="${OVERRIDE_EFFORT:-${F[2]:-}}"
374
442
  if [[ -n "$requested_vendor" ]]; then
375
443
  [[ "$vendor" == "$requested_vendor" ]] || continue
444
+ considered=1
445
+ if [[ "$AA_POLICY_ACTIVE" -eq 1 ]] &&
446
+ ! aa_policy_candidate_allowed "$vendor" "$model" "$effort"; then
447
+ return 6
448
+ fi
376
449
  RESOLVED_SPEC="$seg"; RESOLVED_FIELDS=("${F[@]}"); RESOLVED_IDX="$i"
377
- if routing_candidate_available "$vendor" "${F[1]:-}"; then return 0; fi
450
+ AA_SELECTED_DECISION="$AA_LAST_DECISION"
451
+ if [[ "$RESOLVE_WITH_CONTEXT" -eq 1 ]] || routing_candidate_available "$vendor" "$model"; then return 0; fi
378
452
  return 4
379
453
  fi
380
- if [[ "$vendor" == "off" ]] || routing_candidate_available "$vendor" "${F[1]:-}"; then
454
+ if [[ "$AA_EXPLICIT_TARGET" -eq 1 && "$considered" -eq 1 ]]; then
455
+ return 6
456
+ fi
457
+ considered=1
458
+ if [[ "$AA_POLICY_ACTIVE" -eq 1 ]] &&
459
+ ! aa_policy_candidate_allowed "$vendor" "$model" "$effort"; then
460
+ [[ "$AA_EXPLICIT_TARGET" -eq 1 ]] && return 6
461
+ continue
462
+ fi
463
+ if [[ "$RESOLVE_WITH_CONTEXT" -eq 1 || "$vendor" == "off" ]] || routing_candidate_available "$vendor" "$model"; then
381
464
  RESOLVED_SPEC="$seg"; RESOLVED_FIELDS=("${F[@]}")
465
+ AA_SELECTED_DECISION="$AA_LAST_DECISION"
382
466
  RESOLVED_IDX="$i"; return 0
383
467
  fi
468
+ [[ "$AA_EXPLICIT_TARGET" -eq 1 ]] && return 4
384
469
  done
385
470
  [[ -n "$requested_vendor" ]] && return 5
386
471
  return 1
@@ -627,7 +712,9 @@ while [[ $# -gt 0 ]]; do
627
712
  }
628
713
  SESSION_REQUEST="single-shot"; shift ;;
629
714
  --dry-run) DRY_RUN=1; shift ;;
630
- --mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout|--idle-timeout|--thread)
715
+ --operator-asserted-human)
716
+ AA_OPERATOR_ASSERTED_HUMAN=1; shift ;;
717
+ --mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout|--idle-timeout|--thread|--executor|--native-context|--caller-context|--aa-policy|--target-config|--transport-overlay)
631
718
  # Value-taking flags: a missing value must be a clean usage error (exit 2),
632
719
  # not a `set -u` "unbound variable" crash on $2.
633
720
  [[ $# -ge 2 ]] || { echo "omnilane: $1 needs a value" >&2; exit 2; }
@@ -641,6 +728,14 @@ while [[ $# -gt 0 ]]; do
641
728
  --job-timeout) OVERRIDE_JOB_TIMEOUT="$2" ;;
642
729
  --idle-timeout) OVERRIDE_IDLE_TIMEOUT="$2" ;;
643
730
  --thread) THREAD_NAME="$2" ;;
731
+ --executor) EXECUTOR="$2" ;;
732
+ --native-context)
733
+ [[ -n "$2" ]] || { echo "omnilane: native context path is empty" >&2; exit 2; }
734
+ NATIVE_CONTEXT="$2" ;;
735
+ --caller-context) AA_CALLER_CONTEXT="$2" ;;
736
+ --aa-policy) AA_POLICY_FILE="$2" ;;
737
+ --target-config) AA_TARGET_CONFIG="$2" ;;
738
+ --transport-overlay) export OMNILANE_AA_TRANSPORT_OVERLAY="$2" ;;
644
739
  esac
645
740
  shift 2 ;;
646
741
  -*) echo "omnilane: unknown flag" >&2; exit 2 ;;
@@ -686,6 +781,35 @@ if [[ -n "$OVERRIDE_VENDOR" ]] &&
686
781
  fi
687
782
 
688
783
  depth_guard
784
+ command -v python3 >/dev/null 2>&1 || {
785
+ echo "omnilane: exact-AA policy requires Python 3" >&2; exit 2
786
+ }
787
+ [[ "$AA_OPERATOR_ASSERTED_HUMAN" == "0" || "$AA_OPERATOR_ASSERTED_HUMAN" == "1" ]] || {
788
+ echo "omnilane: OMNILANE_AA_OPERATOR_ASSERTED_HUMAN must be 0 or 1" >&2; exit 2
789
+ }
790
+ if [[ "$AA_OPERATOR_ASSERTED_HUMAN" == "1" && -n "$AA_CALLER_CONTEXT" ]]; then
791
+ echo "omnilane: caller context and operator assertion are mutually exclusive" >&2
792
+ exit 2
793
+ fi
794
+ [[ -n "$AA_POLICY_FILE" ]] || { echo "omnilane: AA policy path is empty" >&2; exit 2; }
795
+ if [[ -n "${OMNILANE_AA_TRANSPORT_OVERLAY:-}" ]]; then
796
+ OMNILANE_AA_OVERLAY_SHA256="$(file_sha256 "$OMNILANE_AA_TRANSPORT_OVERLAY")"
797
+ export OMNILANE_AA_OVERLAY_SHA256
798
+ fi
799
+ AA_POLICY_ACTIVE=1
800
+ if [[ -n "$OVERRIDE_VENDOR" || -n "$OVERRIDE_MODEL" || -n "$OVERRIDE_EFFORT" || -n "$AA_TARGET_CONFIG" ]]; then
801
+ AA_EXPLICIT_TARGET=1
802
+ fi
803
+ case "$EXECUTOR" in
804
+ cli) EXECUTOR_REASON="forced-cli" ;;
805
+ auto|native)
806
+ if [[ "$EXECUTOR" == "native" || -n "$NATIVE_CONTEXT" ]]; then
807
+ # Resolve independently of installed executables. Native rejection may
808
+ # fall back only to this exact CLI target, not to the next routing row.
809
+ RESOLVE_WITH_CONTEXT=1
810
+ fi ;;
811
+ *) echo "omnilane: invalid executor (auto|native|cli)" >&2; exit 2 ;;
812
+ esac
689
813
 
690
814
  CHAIN="$(raw_lane_line "$LANE")" || { echo "omnilane: unknown lane '$LANE' (try --list)" >&2; exit 2; }
691
815
  if [[ -n "$OVERRIDE_VENDOR" ]]; then
@@ -699,7 +823,11 @@ if [[ -n "$OVERRIDE_VENDOR" ]]; then
699
823
  echo "omnilane: requested vendor '$OVERRIDE_VENDOR' is configured for lane '$LANE' but its CLI is unavailable" >&2
700
824
  exit 4
701
825
  ;;
702
- 5)
826
+ 6)
827
+ printf '%s\n' "$AA_LAST_DECISION" >&2
828
+ exit 3
829
+ ;;
830
+ 5)
703
831
  echo "omnilane: requested vendor '$OVERRIDE_VENDOR' is not configured for lane '$LANE'" >&2
704
832
  exit 2
705
833
  ;;
@@ -710,11 +838,16 @@ if [[ -n "$OVERRIDE_VENDOR" ]]; then
710
838
  esac
711
839
  fi
712
840
  else
713
- resolve_chain "$CHAIN" || {
714
- echo "omnilane: no vendor CLI available for lane '$LANE' (chain:$CHAIN)." >&2
715
- echo "omnilane: install a vendor CLI or override the lane in ~/.omnilane/routing.local.yaml" >&2
841
+ resolve_rc=0
842
+ resolve_chain "$CHAIN" || resolve_rc=$?
843
+ if [[ "$resolve_rc" -eq 6 ]]; then
844
+ printf '%s\n' "$AA_LAST_DECISION" >&2
845
+ exit 3
846
+ elif [[ "$resolve_rc" -ne 0 ]]; then
847
+ echo "omnilane: no eligible available target for lane '$LANE' (chain:$CHAIN)." >&2
848
+ [[ -z "$AA_LAST_DECISION" ]] || printf '%s\n' "$AA_LAST_DECISION" >&2
716
849
  exit 4
717
- }
850
+ fi
718
851
  fi
719
852
 
720
853
  FIELDS=("${RESOLVED_FIELDS[@]}")
@@ -722,6 +855,61 @@ VENDOR="${FIELDS[0]}"; MODEL="${FIELDS[1]:-}"; EFFORT="${FIELDS[2]:-}"
722
855
  [[ -n "$OVERRIDE_MODEL" ]] && MODEL="$OVERRIDE_MODEL"
723
856
  [[ -n "$OVERRIDE_EFFORT" ]] && EFFORT="$OVERRIDE_EFFORT"
724
857
 
858
+ AA_REGISTRY_SHA256="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["registry_sha256"])' "$AA_SELECTED_DECISION")"
859
+ AA_CALLER_SHA256="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("caller_context_sha256", ""))' "$AA_SELECTED_DECISION")"
860
+ export OMNILANE_AA_POLICY_FILE="$AA_POLICY_FILE"
861
+ export OMNILANE_AA_REGISTRY_SHA256="$AA_REGISTRY_SHA256"
862
+ export OMNILANE_AA_CALLER_CONTEXT="$AA_CALLER_CONTEXT"
863
+ export OMNILANE_AA_CALLER_SHA256="$AA_CALLER_SHA256"
864
+ export OMNILANE_AA_OPERATOR_ASSERTED_HUMAN="$AA_OPERATOR_ASSERTED_HUMAN"
865
+ export OMNILANE_AA_VOTE_PREFLIGHT="$AA_VOTE_PREFLIGHT"
866
+
867
+ # Shared routing fields are fixed before executor selection. Native never
868
+ # sources runners or launches a shell agent.
869
+ TIMEOUT="$OVERRIDE_TIMEOUT"
870
+ if [[ -z "$TIMEOUT" ]]; then
871
+ LANE_TIMEOUT_VAR="OMNILANE_TIMEOUT_$(printf '%s' "${LANE//-/_}" | tr '[:lower:]' '[:upper:]')"
872
+ TIMEOUT="${!LANE_TIMEOUT_VAR:-}"
873
+ fi
874
+ [[ -n "$TIMEOUT" ]] || TIMEOUT="${OMNILANE_TIMEOUT:-600}"
875
+ [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || {
876
+ echo "omnilane: invalid timeout (want a positive integer of seconds)" >&2; exit 2
877
+ }
878
+ if [[ "$RESOLVE_WITH_CONTEXT" -eq 1 ]]; then
879
+ command -v python3 >/dev/null 2>&1 || { echo "omnilane: native protocol requires Python 3" >&2; exit 2; }
880
+ NATIVE_JOB_VAR="OMNILANE_JOB_TIMEOUT_$(printf '%s' "${LANE//-/_}" | tr '[:lower:]' '[:upper:]')"
881
+ NATIVE_ARGS=(route --home "$OMNILANE_HOME" --executor "$EXECUTOR" --lane "$LANE"
882
+ --vendor "$VENDOR" --model="$MODEL" --effort="$EFFORT" --workdir "$WORKDIR"
883
+ --mode "$MODE" --task="$TASK" --session "$SESSION_REQUEST" --thread "$THREAD_NAME"
884
+ --policy "$AA_POLICY_FILE" --expected-registry-sha256 "$AA_REGISTRY_SHA256"
885
+ --timeout "$TIMEOUT" --job-timeout "${OVERRIDE_JOB_TIMEOUT:-${!NATIVE_JOB_VAR:-${OMNILANE_JOB_TIMEOUT:-}}}"
886
+ --idle-timeout "${OVERRIDE_IDLE_TIMEOUT:-${OMNILANE_IDLE_TIMEOUT:-}}")
887
+ [[ -z "$NATIVE_CONTEXT" ]] || NATIVE_ARGS+=(--context "$NATIVE_CONTEXT")
888
+ [[ -z "$AA_TARGET_CONFIG" ]] || NATIVE_ARGS+=(--target-config "$AA_TARGET_CONFIG")
889
+ if [[ "$AA_OPERATOR_ASSERTED_HUMAN" == "1" ]]; then
890
+ NATIVE_ARGS+=(--operator-asserted-human)
891
+ elif [[ -n "$AA_CALLER_CONTEXT" ]]; then
892
+ NATIVE_ARGS+=(--caller-context "$AA_CALLER_CONTEXT")
893
+ [[ -z "$AA_CALLER_SHA256" ]] || NATIVE_ARGS+=(--expected-caller-sha256 "$AA_CALLER_SHA256")
894
+ fi
895
+ [[ "$BACKGROUND" -eq 0 ]] || NATIVE_ARGS+=(--background)
896
+ [[ "$DRY_RUN" -eq 0 ]] || NATIVE_ARGS+=(--dry-run)
897
+ NATIVE_RC=0
898
+ NATIVE_OUTPUT="$(python3 "$OMNILANE_REPO/scripts/lib/native.py" "${NATIVE_ARGS[@]}")" || NATIVE_RC=$?
899
+ if [[ "$NATIVE_RC" -eq 0 ]]; then
900
+ printf '%s\n' "$NATIVE_OUTPUT"
901
+ exit 0
902
+ elif [[ "$NATIVE_RC" -ne 10 ]]; then
903
+ exit "$NATIVE_RC"
904
+ fi
905
+ EXECUTOR_REASON="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["reason"])' "$NATIVE_OUTPUT")"
906
+ MODEL="$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["model"])' "$NATIVE_OUTPUT")"
907
+ [[ "$VENDOR" == "off" ]] || routing_candidate_available "$VENDOR" "$MODEL" || {
908
+ echo "omnilane: native rejected ($EXECUTOR_REASON); exact CLI target '$VENDOR' unavailable; no vendor/model substitution" >&2
909
+ exit 4
910
+ }
911
+ fi
912
+
725
913
  if [[ -n "$THREAD_NAME" ]]; then
726
914
  case "$VENDOR" in
727
915
  claude|codex|grok|gemini) ;;
@@ -785,13 +973,32 @@ fi
785
973
  RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
786
974
  [[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
787
975
 
976
+ # Fail closed before creating job state or starting a provider when the selected
977
+ # runtime cannot enforce the requested mode. Dry-run reports the same gap.
978
+ if [[ "$VENDOR" == "grok" && "$MODE" == "work" && "$(uname -s)" == "Darwin" ]]; then
979
+ echo "omnilane: Grok work requires disabled agent-tool network; xAI CLI child-network restrictions are not enforced on macOS" >&2
980
+ exit 2
981
+ fi
982
+
788
983
  SESSION_MODE="single-shot"
789
- if [[ -z "$THREAD_NAME" && "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR"; then
790
- SESSION_MODE="live"
984
+ if [[ -z "$THREAD_NAME" && "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR" &&
985
+ [[ "$SESSION_REQUEST" == "live" || "$VENDOR" == "claude" || "$VENDOR" == "gemini" ]]; then
986
+ # Preserve established auto behavior: Codex/Grok require an explicit --live.
987
+ # Claude/Gemini retain their existing auto-selected resident sessions.
988
+ # Grok ACP currently exposes no enforceable restricted policy.
989
+ # Only explicit sysops dispatch may use its resident ACP session.
990
+ if [[ "$VENDOR" != "grok" || "$MODE" == "sysops" ]]; then
991
+ SESSION_MODE="live"
992
+ fi
791
993
  fi
792
- if [[ "$SESSION_REQUEST" == "live" ]] && ! live_vendor_capable "$VENDOR"; then
793
- echo "omnilane: vendor '$VENDOR' cannot run live; live-capable vendors: $(live_capable_vendors)" >&2
794
- exit 2
994
+ if [[ "$SESSION_REQUEST" == "live" ]]; then
995
+ if [[ "$VENDOR" == "grok" && "$MODE" != "sysops" ]]; then
996
+ echo "omnilane: Grok --live supports only explicit --mode sysops; restricted ACP policy is unavailable" >&2
997
+ exit 2
998
+ elif ! live_vendor_capable "$VENDOR"; then
999
+ echo "omnilane: vendor '$VENDOR' cannot run live; live-capable vendors: $(live_capable_vendors)" >&2
1000
+ exit 2
1001
+ fi
795
1002
  fi
796
1003
  export OMNILANE_SESSION_MODE="$SESSION_MODE"
797
1004
  if [[ "$SESSION_REQUEST" == "live" ]]; then
@@ -804,15 +1011,6 @@ fi
804
1011
  # Resolve here and export so every runner (and vote's sub-runners) inherits the
805
1012
  # same value without a per-runner code change; they already read OMNILANE_TIMEOUT.
806
1013
  # This bounds each runner CLI call, not the whole dispatch (see header note).
807
- TIMEOUT="$OVERRIDE_TIMEOUT"
808
- if [[ -z "$TIMEOUT" ]]; then
809
- LANE_TIMEOUT_VAR="OMNILANE_TIMEOUT_$(printf '%s' "${LANE//-/_}" | tr '[:lower:]' '[:upper:]')"
810
- TIMEOUT="${!LANE_TIMEOUT_VAR:-}"
811
- fi
812
- [[ -n "$TIMEOUT" ]] || TIMEOUT="${OMNILANE_TIMEOUT:-600}"
813
- [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || {
814
- echo "omnilane: invalid timeout (want a positive integer of seconds)" >&2; exit 2
815
- }
816
1014
  export OMNILANE_TIMEOUT="$TIMEOUT"
817
1015
 
818
1016
  IDLE_TIMEOUT="$OVERRIDE_IDLE_TIMEOUT"
@@ -822,9 +1020,20 @@ IDLE_TIMEOUT="$OVERRIDE_IDLE_TIMEOUT"
822
1020
  exit 2
823
1021
  }
824
1022
  export OMNILANE_IDLE_TIMEOUT="$IDLE_TIMEOUT"
1023
+ # The idle cap only exists inside a live mailbox, so a single-shot dispatch
1024
+ # would be told about a limit that never applies to it.
1025
+ if [[ "$SESSION_MODE" == "live" && "$IDLE_TIMEOUT" -gt 0 && "$TIMEOUT" -gt "$IDLE_TIMEOUT" ]]; then
1026
+ echo "omnilane: --timeout is ${TIMEOUT}s, but the independent live mailbox idle cap remains ${IDLE_TIMEOUT}s; adjust it with --idle-timeout SECONDS" >&2
1027
+ fi
825
1028
 
826
1029
  JOB_SUPERVISOR="$OMNILANE_REPO/scripts/lib/job-timeout.pl"
827
- JOB_WORKER="$OMNILANE_REPO/scripts/lib/job-worker.sh"
1030
+ JOB_WORKER_SOURCE="$OMNILANE_REPO/scripts/lib/job-worker.sh"
1031
+ # Pinned deliberately: /bin/bash is 3.2 on macOS while a PATH bash is typically
1032
+ # 5.x, so the worker and every library it sources must stay bash-3.2 compatible.
1033
+ # No declare -A, mapfile, ${v,,}, [[ -v ]], ;;&, EPOCHSECONDS or coproc.
1034
+ JOB_WORKER_BASH="/bin/bash"
1035
+ [[ -x "$JOB_WORKER_BASH" ]] || { echo "omnilane: fixed worker interpreter is unavailable: $JOB_WORKER_BASH" >&2; exit 2; }
1036
+ JOB_WORKER_BASH_VERSION="$($JOB_WORKER_BASH -c 'printf %s "$BASH_VERSION"')"
828
1037
 
829
1038
  # Optional whole-job seconds: flag > per-lane env > global env > automatic
830
1039
  # non-Git Codex work guard > disabled.
@@ -870,7 +1079,7 @@ if [[ -n "$JOB_TIMEOUT" && ! "$JOB_TIMEOUT" =~ ^[1-9][0-9]{0,8}$ ]]; then
870
1079
  fi
871
1080
  JOB_TIMEOUT_JSON="${JOB_TIMEOUT:-null}"
872
1081
  unset OMNILANE_JOB_SUPERVISED
873
- [[ -x "$JOB_WORKER" ]] || { echo "omnilane: internal job worker is unavailable" >&2; exit 2; }
1082
+ [[ -x "$JOB_WORKER_SOURCE" ]] || { echo "omnilane: internal job worker is unavailable" >&2; exit 2; }
874
1083
  if [[ -n "$JOB_TIMEOUT" ]]; then
875
1084
  [[ -f "$JOB_SUPERVISOR" ]] || { echo "omnilane: whole-job timeout supervisor is unavailable" >&2; exit 2; }
876
1085
  command -v perl &>/dev/null || { echo "omnilane: --job-timeout requires perl" >&2; exit 2; }
@@ -889,6 +1098,7 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
889
1098
  exit 0
890
1099
  fi
891
1100
 
1101
+ printf 'omnilane: executor=cli reason=%s\n' "$EXECUTOR_REASON" >&2
892
1102
  mkdir -p "$OMNILANE_HOME"
893
1103
  if [[ ! -d "$JOBS_ROOT" ]]; then
894
1104
  mkdir -m 700 "$JOBS_ROOT"
@@ -904,6 +1114,58 @@ chmod 700 "$JOBS_ROOT"
904
1114
  JOB_ID="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
905
1115
  JOB_DIR="$JOBS_ROOT/$JOB_ID"
906
1116
  mkdir -m 700 "$JOB_DIR"
1117
+ # Snapshot authorizer separately from the worker identity. Never hand a worker
1118
+ # its parent's identity as its own caller context.
1119
+ python3 - "$OMNILANE_REPO" "$JOB_DIR" "$AA_POLICY_FILE" "$AA_REGISTRY_SHA256" "$AA_CALLER_CONTEXT" "$AA_CALLER_SHA256" "$AA_SELECTED_DECISION" "$VENDOR" <<'AA_PUBLISH'
1120
+ import json, sys
1121
+ from pathlib import Path
1122
+ sys.path.insert(0, str(Path(sys.argv[1]) / "scripts/lib"))
1123
+ import aa_policy
1124
+ _, repo, directory, policy, policy_sha, context, context_sha, raw, vendor = sys.argv
1125
+ registry, _ = aa_policy.load_registry(policy, policy_sha)
1126
+ caller = aa_policy.load_caller(context, registry, context_sha)[0] if context else None
1127
+ decision = json.loads(raw)
1128
+ if vendor == "vote":
1129
+ decision["target_config_id"] = None
1130
+ decision["child_context"] = None
1131
+ decision["target_request"] = {"vendor": "vote", "model": None, "effort": None}
1132
+ aa_policy.publish_context(directory, registry, caller, decision, policy)
1133
+ AA_PUBLISH
1134
+ AA_POLICY_FILE="$JOB_DIR/aa-registry.json"
1135
+ AA_REGISTRY_SHA256="$(file_sha256 "$AA_POLICY_FILE")"
1136
+ AA_CALLER_CONTEXT=""
1137
+ AA_CALLER_SHA256=""
1138
+ if [[ -f "$JOB_DIR/aa-authorizer.json" ]]; then
1139
+ AA_CALLER_CONTEXT="$JOB_DIR/aa-authorizer.json"
1140
+ AA_CALLER_SHA256="$(file_sha256 "$AA_CALLER_CONTEXT")"
1141
+ fi
1142
+ export OMNILANE_AA_POLICY_FILE="$AA_POLICY_FILE"
1143
+ export OMNILANE_AA_REGISTRY_SHA256="$AA_REGISTRY_SHA256"
1144
+ export OMNILANE_AA_AUTHORIZER_CONTEXT="$AA_CALLER_CONTEXT"
1145
+ export OMNILANE_AA_AUTHORIZER_SHA256="$AA_CALLER_SHA256"
1146
+ export OMNILANE_AA_AUTHORIZER_HUMAN="$AA_OPERATOR_ASSERTED_HUMAN"
1147
+ export OMNILANE_AA_CONTEXT_DIR="$JOB_DIR"
1148
+ export OMNILANE_AA_TARGET_CONFIG="$AA_TARGET_CONFIG"
1149
+ # Human exemptions belong only to this operator launch, never to a model child.
1150
+ unset OMNILANE_AA_OPERATOR_ASSERTED_HUMAN
1151
+ export OMNILANE_AA_CALLER_CONTEXT=""
1152
+ export OMNILANE_AA_CALLER_SHA256=""
1153
+ if [[ -f "$JOB_DIR/aa-child-context.json" ]]; then
1154
+ export OMNILANE_AA_CALLER_CONTEXT="$JOB_DIR/aa-child-context.json"
1155
+ OMNILANE_AA_CALLER_SHA256="$(file_sha256 "$JOB_DIR/aa-child-context.json")"
1156
+ export OMNILANE_AA_CALLER_SHA256
1157
+ fi
1158
+ JOB_WORKER="$JOB_DIR/job-worker.sh"
1159
+ JOB_WORKER_TMP="$JOB_DIR/.job-worker.tmp.$$-$RANDOM"
1160
+ (umask 077; cat "$JOB_WORKER_SOURCE" > "$JOB_WORKER_TMP")
1161
+ chmod 400 "$JOB_WORKER_TMP"
1162
+ mv "$JOB_WORKER_TMP" "$JOB_WORKER"
1163
+ JOB_WORKER_SOURCE_SHA256="$(file_sha256 "$JOB_WORKER_SOURCE")" || exit 2
1164
+ JOB_WORKER_SHA256="$(file_sha256 "$JOB_WORKER")" || exit 2
1165
+ if [[ "$JOB_WORKER_SHA256" != "$JOB_WORKER_SOURCE_SHA256" ]]; then
1166
+ echo "omnilane: job worker source changed while creating the per-job snapshot" >&2
1167
+ exit 1
1168
+ fi
907
1169
  FOREMAN_SESSION=""
908
1170
  if [[ -n "${CLAUDE_CODE_SESSION_ID+x}" ]]; then
909
1171
  if [[ "$CLAUDE_CODE_SESSION_ID" =~ ^[A-Za-z0-9._:-]+$ &&
@@ -941,6 +1203,25 @@ else
941
1203
  "$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
942
1204
  fi
943
1205
 
1206
+ META_TMP="$JOB_DIR/.meta.tmp.$$-$RANDOM"
1207
+ META_BASE="$(cat "$JOB_DIR/meta.json")"
1208
+ if [[ "$META_BASE" != *'}' ]]; then
1209
+ rm "$META_TMP" 2>/dev/null || true
1210
+ echo "omnilane: cannot add worker provenance to malformed meta.json" >&2
1211
+ exit 1
1212
+ fi
1213
+ META_BASE="${META_BASE%\}}"
1214
+ META_BASE="$META_BASE,\"executor\":\"cli\",\"executor_reason\":\"$(json_escape "$EXECUTOR_REASON")\""
1215
+ printf '%s,"worker_interpreter_path":"%s","worker_interpreter_version":"%s","job_worker_source_path":"%s","job_worker_source_sha256":"%s","job_worker_path":"%s","job_worker_sha256":"%s"}\n' \
1216
+ "$META_BASE" "$(json_escape "$JOB_WORKER_BASH")" \
1217
+ "$(json_escape "$JOB_WORKER_BASH_VERSION")" "$(json_escape "$JOB_WORKER_SOURCE")" \
1218
+ "$(json_escape "$JOB_WORKER_SOURCE_SHA256")" "$(json_escape "$JOB_WORKER")" \
1219
+ "$(json_escape "$JOB_WORKER_SHA256")" \
1220
+ > "$META_TMP"
1221
+ json_file_round_trip_valid "$META_TMP" || exit 1
1222
+ chmod 600 "$META_TMP"
1223
+ mv "$META_TMP" "$JOB_DIR/meta.json"
1224
+
944
1225
  if [[ -n "$THREAD_NAME" ]]; then
945
1226
  printf 'omnilane: thread %s turn %s (%s session %s, %s)\n' \
946
1227
  "$THREAD_NAME" "$THREAD_TURN" "$VENDOR" "$THREAD_ID" "$THREAD_MODE"
@@ -948,6 +1229,9 @@ fi
948
1229
 
949
1230
  secure_job_files() {
950
1231
  find "$JOB_DIR" -type f -exec chmod 600 {} +
1232
+ if [[ -f "$JOB_WORKER" && ! -L "$JOB_WORKER" ]]; then
1233
+ chmod 400 "$JOB_WORKER"
1234
+ fi
951
1235
  }
952
1236
 
953
1237
  sanitize_utf8() {
@@ -1023,6 +1307,9 @@ write_completion_record() {
1023
1307
  "$rc" "$(json_escape "$finished")" "$(json_escape "$tail_value")" \
1024
1308
  > "$tmp" || write_rc=$?
1025
1309
  fi
1310
+ if [[ "$write_rc" -eq 0 ]] && ! json_file_round_trip_valid "$tmp"; then
1311
+ write_rc=1
1312
+ fi
1026
1313
  if [[ "$write_rc" -eq 0 ]]; then
1027
1314
  chmod 600 "$tmp" || write_rc=$?
1028
1315
  fi
@@ -1047,7 +1334,7 @@ finish_job() {
1047
1334
  secure_job_files
1048
1335
  (umask 077; printf '%s\n' "$rc" > "$JOB_DIR/exit")
1049
1336
  if [[ "${OMNILANE_INBOX:-1}" != "0" ]]; then
1050
- write_completion_record "$rc" >/dev/null 2>&1 || true
1337
+ write_completion_record "$rc" >/dev/null || true
1051
1338
  fi
1052
1339
  FINISHED_RC="$rc"
1053
1340
  }
@@ -1057,11 +1344,15 @@ run_job() {
1057
1344
  write_current_pid_file "$JOB_DIR/pid"
1058
1345
  set +e
1059
1346
  if [[ -n "$JOB_TIMEOUT" ]]; then
1060
- OMNILANE_JOB_SUPERVISED=1 perl "$JOB_SUPERVISOR" "$JOB_TIMEOUT" \
1061
- "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1347
+ OMNILANE_JOB_WORKER_REPO="$OMNILANE_REPO" \
1348
+ OMNILANE_JOB_WORKER_EXPECTED_SHA256="$JOB_WORKER_SHA256" \
1349
+ OMNILANE_JOB_SUPERVISED=1 perl "$JOB_SUPERVISOR" "$JOB_TIMEOUT" \
1350
+ "$JOB_WORKER_BASH" "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1062
1351
  "$JOB_DIR/task.txt" "$JOB_DIR/out.txt"
1063
1352
  else
1064
- "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1353
+ OMNILANE_JOB_WORKER_REPO="$OMNILANE_REPO" \
1354
+ OMNILANE_JOB_WORKER_EXPECTED_SHA256="$JOB_WORKER_SHA256" \
1355
+ "$JOB_WORKER_BASH" "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1065
1356
  "$JOB_DIR/task.txt" "$JOB_DIR/out.txt"
1066
1357
  fi
1067
1358
  rc=$?
package/scripts/doctor.sh CHANGED
@@ -28,10 +28,13 @@ done
28
28
  echo "omnilane: --probe-timeout requires --probe V" >&2
29
29
  exit 2
30
30
  }
31
- REPO="${OMNILANE_DOCTOR_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
31
+ SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
32
+ REPO="${OMNILANE_DOCTOR_REPO:-$SCRIPT_ROOT}"
32
33
  OMNILANE_HOME="${OMNILANE_HOME:-$HOME/.omnilane}"
33
34
  PROBE_SCRIPT="${OMNILANE_PROVIDER_PROBE_SCRIPT:-$REPO/scripts/provider-probe.sh}"
34
35
  GOAL_LOOP="${OMNILANE_DOCTOR_GOAL_LOOP:-$REPO/scripts/lib/goal-loop.sh}"
36
+ # shellcheck disable=SC1091
37
+ source "$SCRIPT_ROOT/scripts/lib/live-protocol.sh"
35
38
  PASS_COUNT=0
36
39
  WARN_COUNT=0
37
40
  FAIL_COUNT=0
@@ -355,6 +358,57 @@ fi
355
358
 
356
359
  # A real inference call is explicit and single-vendor only. Default doctor
357
360
  # remains local/offline. Never relay provider output or runner stderr.
361
+ live_capable_present=""
362
+ live_unavailable_present=""
363
+ resolved_codex_bin="$(
364
+ set +u
365
+ [[ -f "$OMNILANE_HOME/local.sh" ]] && . "$OMNILANE_HOME/local.sh" 2>/dev/null
366
+ printf '%s' "${CODEX_BIN:-codex}"
367
+ )"
368
+ resolved_grok_bin="$(
369
+ set +u
370
+ [[ -f "$OMNILANE_HOME/local.sh" ]] && . "$OMNILANE_HOME/local.sh" 2>/dev/null
371
+ printf '%s' "${GROK_BIN:-grok}"
372
+ )"
373
+ for name in $vendor_present; do
374
+ case "$name" in
375
+ claude|gemini)
376
+ live_capable_present="$live_capable_present $name"
377
+ ;;
378
+ codex)
379
+ if codex_live_surface_available "$resolved_codex_bin"; then
380
+ live_capable_present="$live_capable_present codex"
381
+ report PASS codex-live "app-server initialize handshake succeeded"
382
+ else
383
+ live_unavailable_present="$live_unavailable_present codex"
384
+ report PASS codex-live "unavailable: app-server initialize handshake failed; upgrade codex"
385
+ fi
386
+ ;;
387
+ grok)
388
+ if grok_live_surface_available "$resolved_grok_bin"; then
389
+ live_capable_present="$live_capable_present grok"
390
+ report PASS grok-live "agent stdio initialize handshake succeeded"
391
+ else
392
+ live_unavailable_present="$live_unavailable_present grok"
393
+ report PASS grok-live "unavailable: agent stdio initialize handshake failed"
394
+ fi
395
+ ;;
396
+ *)
397
+ live_unavailable_present="$live_unavailable_present $name"
398
+ ;;
399
+ esac
400
+ done
401
+ if [[ -n "$live_capable_present" ]]; then
402
+ report PASS live-capable "${live_capable_present# }"
403
+ else
404
+ report WARN live-capable "none"
405
+ fi
406
+ if [[ -n "$live_unavailable_present" ]]; then
407
+ report PASS live-unavailable "${live_unavailable_present# }"
408
+ else
409
+ report PASS live-unavailable "none"
410
+ fi
411
+
358
412
  if [[ -n "$PROBE_VENDOR" ]]; then
359
413
  if [[ ! -x "$PROBE_SCRIPT" ]]; then
360
414
  report FAIL provider-probe "probe runner is unavailable"