omnilane 0.41.1 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,6 +65,13 @@ 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
77
  or sysops (vendor sandbox disabled, for service
@@ -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) ;;
@@ -823,15 +1011,6 @@ fi
823
1011
  # Resolve here and export so every runner (and vote's sub-runners) inherits the
824
1012
  # same value without a per-runner code change; they already read OMNILANE_TIMEOUT.
825
1013
  # This bounds each runner CLI call, not the whole dispatch (see header note).
826
- TIMEOUT="$OVERRIDE_TIMEOUT"
827
- if [[ -z "$TIMEOUT" ]]; then
828
- LANE_TIMEOUT_VAR="OMNILANE_TIMEOUT_$(printf '%s' "${LANE//-/_}" | tr '[:lower:]' '[:upper:]')"
829
- TIMEOUT="${!LANE_TIMEOUT_VAR:-}"
830
- fi
831
- [[ -n "$TIMEOUT" ]] || TIMEOUT="${OMNILANE_TIMEOUT:-600}"
832
- [[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || {
833
- echo "omnilane: invalid timeout (want a positive integer of seconds)" >&2; exit 2
834
- }
835
1014
  export OMNILANE_TIMEOUT="$TIMEOUT"
836
1015
 
837
1016
  IDLE_TIMEOUT="$OVERRIDE_IDLE_TIMEOUT"
@@ -919,6 +1098,7 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
919
1098
  exit 0
920
1099
  fi
921
1100
 
1101
+ printf 'omnilane: executor=cli reason=%s\n' "$EXECUTOR_REASON" >&2
922
1102
  mkdir -p "$OMNILANE_HOME"
923
1103
  if [[ ! -d "$JOBS_ROOT" ]]; then
924
1104
  mkdir -m 700 "$JOBS_ROOT"
@@ -934,6 +1114,47 @@ chmod 700 "$JOBS_ROOT"
934
1114
  JOB_ID="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
935
1115
  JOB_DIR="$JOBS_ROOT/$JOB_ID"
936
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
937
1158
  JOB_WORKER="$JOB_DIR/job-worker.sh"
938
1159
  JOB_WORKER_TMP="$JOB_DIR/.job-worker.tmp.$$-$RANDOM"
939
1160
  (umask 077; cat "$JOB_WORKER_SOURCE" > "$JOB_WORKER_TMP")
@@ -990,6 +1211,7 @@ if [[ "$META_BASE" != *'}' ]]; then
990
1211
  exit 1
991
1212
  fi
992
1213
  META_BASE="${META_BASE%\}}"
1214
+ META_BASE="$META_BASE,\"executor\":\"cli\",\"executor_reason\":\"$(json_escape "$EXECUTOR_REASON")\""
993
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' \
994
1216
  "$META_BASE" "$(json_escape "$JOB_WORKER_BASH")" \
995
1217
  "$(json_escape "$JOB_WORKER_BASH_VERSION")" "$(json_escape "$JOB_WORKER_SOURCE")" \
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"
@@ -322,6 +324,23 @@ done
322
324
  # set -u on Bash 3.2; a bare `jobs.sh` must reach usage, not crash.
323
325
  set -- ${args[@]+"${args[@]}"}
324
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
325
344
  if [[ "$JSON_MODE" -eq 1 ]]; then
326
345
  case "$COMMAND" in
327
346
  list|status|result|stats|recommend|audit|threads) ;;
@@ -511,6 +530,15 @@ case "${1:-}" in
511
530
  while [[ $# -gt 0 ]]; do
512
531
  case "$1" in
513
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 ;;
514
542
  *) usage ;;
515
543
  esac
516
544
  done
@@ -521,18 +549,14 @@ case "${1:-}" in
521
549
  read_public_metadata "$JOB_DIR/meta.json" || {
522
550
  die 1 "cannot retry: unreadable job metadata"
523
551
  }
524
- retry_re='^\{"lane":"([a-z][a-z0-9-]*)","vendor":"('"$OMNILANE_VENDOR_ALT"')","model":"([^"\\]*)","effort":"([^"\\]*)","timeout":([1-9][0-9]*),"job_timeout":([1-9][0-9]*|null),"mode":"(advise|work)","workdir":"([^"\\]*)",'
525
- [[ "$PUBLIC_METADATA" =~ $retry_re ]] || {
526
- die 1 "cannot retry: job metadata is not safely parseable"
527
- }
528
- retry_lane="${BASH_REMATCH[1]}"
529
- retry_vendor="${BASH_REMATCH[2]}"
530
- retry_model="${BASH_REMATCH[3]}"
531
- retry_effort="${BASH_REMATCH[4]}"
532
- retry_timeout="${BASH_REMATCH[5]}"
533
- retry_job_timeout="${BASH_REMATCH[6]}"
534
- retry_mode="${BASH_REMATCH[7]}"
535
- 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]}"
536
560
  task_path="$JOB_DIR/task.txt"
537
561
  [[ -f "$task_path" && ! -L "$task_path" ]] || {
538
562
  die 1 "cannot retry: original task text is missing"
@@ -541,6 +565,13 @@ case "${1:-}" in
541
565
  die 1 "cannot retry: original workdir no longer exists: $retry_workdir"
542
566
  }
543
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
+
544
575
  [[ "$retry_job_timeout" == "null" ]] || retry_args+=(--job-timeout "$retry_job_timeout")
545
576
  # dispatch --vendor only accepts real CLI vendors; an exec gate is re-run
546
577
  # through normal lane resolution plus the recorded --model script path.
@@ -892,8 +923,8 @@ case "${1:-}" in
892
923
  [[ -z "$filter_lane" || "$filter_lane" =~ ^[a-z][a-z0-9-]*$ ]] || die 2 "invalid --lane value"
893
924
  [[ -z "$filter_vendor" ]] || omnilane_known_vendor "$filter_vendor" || die 2 "invalid --vendor value"
894
925
  case "$filter_status" in
895
- ""|running|done) ;;
896
- *) die 2 "invalid --status value (want running or done)" ;;
926
+ ""|running|done|pending|cancelled) ;;
927
+ *) die 2 "invalid --status value (want running, done, pending or cancelled)" ;;
897
928
  esac
898
929
  if [[ ! -d "$JOBS" ]]; then
899
930
  [[ "$JSON_MODE" -eq 0 ]] || printf '{"schema_version":1,"command":"list","ok":true,"jobs":[]}\n'
@@ -929,6 +960,16 @@ case "${1:-}" in
929
960
  else
930
961
  json_state="running"
931
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
932
973
  metadata=""
933
974
  metadata_json="null"
934
975
  metadata_status="missing"
@@ -953,6 +994,7 @@ case "${1:-}" in
953
994
  case "$json_state" in
954
995
  done) [[ "$filter_status" == "done" ]] || continue ;;
955
996
  running) [[ "$filter_status" == "running" ]] || continue ;;
997
+ pending|cancelled) [[ "$filter_status" == "$json_state" ]] || continue ;;
956
998
  *) continue ;;
957
999
  esac
958
1000
  fi
@@ -1229,6 +1271,7 @@ case "${1:-}" in
1229
1271
  [[ -d "$job_dir" && ! -L "$job_dir" ]] || continue
1230
1272
  id="${job_dir##*/}"
1231
1273
  [[ "$id" =~ $JOB_ID_PATTERN ]] || continue
1274
+ [[ ! -e "$job_dir/native.lock" && ! -e "$job_dir/native.json" ]] || continue
1232
1275
  read_exit_code "$job_dir/exit" || continue
1233
1276
  completed+=("$id")
1234
1277
  done