omnilane 0.34.0 → 0.41.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.
@@ -60,9 +60,9 @@ flags:
60
60
  before any provider call or job state
61
61
  --mode advise|work|sysops
62
62
  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)
63
+ or sysops (vendor sandbox disabled, for service
64
+ operations like launchctl; supported by
65
+ codex, claude, grok, and gemini)
66
66
  --workdir DIR working directory handed to the vendor CLI
67
67
  --vendor V pin one configured vendor (codex|claude|grok|gemini|kimi|qwen|opencode|openrouter|deepseek|zai|mistral|groq|cerebras)
68
68
  --model M override the routed model
@@ -785,13 +785,32 @@ fi
785
785
  RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
786
786
  [[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
787
787
 
788
+ # Fail closed before creating job state or starting a provider when the selected
789
+ # runtime cannot enforce the requested mode. Dry-run reports the same gap.
790
+ if [[ "$VENDOR" == "grok" && "$MODE" == "work" && "$(uname -s)" == "Darwin" ]]; then
791
+ echo "omnilane: Grok work requires disabled agent-tool network; xAI CLI child-network restrictions are not enforced on macOS" >&2
792
+ exit 2
793
+ fi
794
+
788
795
  SESSION_MODE="single-shot"
789
- if [[ -z "$THREAD_NAME" && "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR"; then
790
- SESSION_MODE="live"
796
+ if [[ -z "$THREAD_NAME" && "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR" &&
797
+ [[ "$SESSION_REQUEST" == "live" || "$VENDOR" == "claude" || "$VENDOR" == "gemini" ]]; then
798
+ # Preserve established auto behavior: Codex/Grok require an explicit --live.
799
+ # Claude/Gemini retain their existing auto-selected resident sessions.
800
+ # Grok ACP currently exposes no enforceable restricted policy.
801
+ # Only explicit sysops dispatch may use its resident ACP session.
802
+ if [[ "$VENDOR" != "grok" || "$MODE" == "sysops" ]]; then
803
+ SESSION_MODE="live"
804
+ fi
791
805
  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
806
+ if [[ "$SESSION_REQUEST" == "live" ]]; then
807
+ if [[ "$VENDOR" == "grok" && "$MODE" != "sysops" ]]; then
808
+ echo "omnilane: Grok --live supports only explicit --mode sysops; restricted ACP policy is unavailable" >&2
809
+ exit 2
810
+ elif ! live_vendor_capable "$VENDOR"; then
811
+ echo "omnilane: vendor '$VENDOR' cannot run live; live-capable vendors: $(live_capable_vendors)" >&2
812
+ exit 2
813
+ fi
795
814
  fi
796
815
  export OMNILANE_SESSION_MODE="$SESSION_MODE"
797
816
  if [[ "$SESSION_REQUEST" == "live" ]]; then
@@ -822,9 +841,20 @@ IDLE_TIMEOUT="$OVERRIDE_IDLE_TIMEOUT"
822
841
  exit 2
823
842
  }
824
843
  export OMNILANE_IDLE_TIMEOUT="$IDLE_TIMEOUT"
844
+ # The idle cap only exists inside a live mailbox, so a single-shot dispatch
845
+ # would be told about a limit that never applies to it.
846
+ if [[ "$SESSION_MODE" == "live" && "$IDLE_TIMEOUT" -gt 0 && "$TIMEOUT" -gt "$IDLE_TIMEOUT" ]]; then
847
+ echo "omnilane: --timeout is ${TIMEOUT}s, but the independent live mailbox idle cap remains ${IDLE_TIMEOUT}s; adjust it with --idle-timeout SECONDS" >&2
848
+ fi
825
849
 
826
850
  JOB_SUPERVISOR="$OMNILANE_REPO/scripts/lib/job-timeout.pl"
827
- JOB_WORKER="$OMNILANE_REPO/scripts/lib/job-worker.sh"
851
+ JOB_WORKER_SOURCE="$OMNILANE_REPO/scripts/lib/job-worker.sh"
852
+ # Pinned deliberately: /bin/bash is 3.2 on macOS while a PATH bash is typically
853
+ # 5.x, so the worker and every library it sources must stay bash-3.2 compatible.
854
+ # No declare -A, mapfile, ${v,,}, [[ -v ]], ;;&, EPOCHSECONDS or coproc.
855
+ JOB_WORKER_BASH="/bin/bash"
856
+ [[ -x "$JOB_WORKER_BASH" ]] || { echo "omnilane: fixed worker interpreter is unavailable: $JOB_WORKER_BASH" >&2; exit 2; }
857
+ JOB_WORKER_BASH_VERSION="$($JOB_WORKER_BASH -c 'printf %s "$BASH_VERSION"')"
828
858
 
829
859
  # Optional whole-job seconds: flag > per-lane env > global env > automatic
830
860
  # non-Git Codex work guard > disabled.
@@ -870,7 +900,7 @@ if [[ -n "$JOB_TIMEOUT" && ! "$JOB_TIMEOUT" =~ ^[1-9][0-9]{0,8}$ ]]; then
870
900
  fi
871
901
  JOB_TIMEOUT_JSON="${JOB_TIMEOUT:-null}"
872
902
  unset OMNILANE_JOB_SUPERVISED
873
- [[ -x "$JOB_WORKER" ]] || { echo "omnilane: internal job worker is unavailable" >&2; exit 2; }
903
+ [[ -x "$JOB_WORKER_SOURCE" ]] || { echo "omnilane: internal job worker is unavailable" >&2; exit 2; }
874
904
  if [[ -n "$JOB_TIMEOUT" ]]; then
875
905
  [[ -f "$JOB_SUPERVISOR" ]] || { echo "omnilane: whole-job timeout supervisor is unavailable" >&2; exit 2; }
876
906
  command -v perl &>/dev/null || { echo "omnilane: --job-timeout requires perl" >&2; exit 2; }
@@ -904,6 +934,17 @@ chmod 700 "$JOBS_ROOT"
904
934
  JOB_ID="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
905
935
  JOB_DIR="$JOBS_ROOT/$JOB_ID"
906
936
  mkdir -m 700 "$JOB_DIR"
937
+ JOB_WORKER="$JOB_DIR/job-worker.sh"
938
+ JOB_WORKER_TMP="$JOB_DIR/.job-worker.tmp.$$-$RANDOM"
939
+ (umask 077; cat "$JOB_WORKER_SOURCE" > "$JOB_WORKER_TMP")
940
+ chmod 400 "$JOB_WORKER_TMP"
941
+ mv "$JOB_WORKER_TMP" "$JOB_WORKER"
942
+ JOB_WORKER_SOURCE_SHA256="$(file_sha256 "$JOB_WORKER_SOURCE")" || exit 2
943
+ JOB_WORKER_SHA256="$(file_sha256 "$JOB_WORKER")" || exit 2
944
+ if [[ "$JOB_WORKER_SHA256" != "$JOB_WORKER_SOURCE_SHA256" ]]; then
945
+ echo "omnilane: job worker source changed while creating the per-job snapshot" >&2
946
+ exit 1
947
+ fi
907
948
  FOREMAN_SESSION=""
908
949
  if [[ -n "${CLAUDE_CODE_SESSION_ID+x}" ]]; then
909
950
  if [[ "$CLAUDE_CODE_SESSION_ID" =~ ^[A-Za-z0-9._:-]+$ &&
@@ -941,6 +982,24 @@ else
941
982
  "$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
942
983
  fi
943
984
 
985
+ META_TMP="$JOB_DIR/.meta.tmp.$$-$RANDOM"
986
+ META_BASE="$(cat "$JOB_DIR/meta.json")"
987
+ if [[ "$META_BASE" != *'}' ]]; then
988
+ rm "$META_TMP" 2>/dev/null || true
989
+ echo "omnilane: cannot add worker provenance to malformed meta.json" >&2
990
+ exit 1
991
+ fi
992
+ META_BASE="${META_BASE%\}}"
993
+ 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
+ "$META_BASE" "$(json_escape "$JOB_WORKER_BASH")" \
995
+ "$(json_escape "$JOB_WORKER_BASH_VERSION")" "$(json_escape "$JOB_WORKER_SOURCE")" \
996
+ "$(json_escape "$JOB_WORKER_SOURCE_SHA256")" "$(json_escape "$JOB_WORKER")" \
997
+ "$(json_escape "$JOB_WORKER_SHA256")" \
998
+ > "$META_TMP"
999
+ json_file_round_trip_valid "$META_TMP" || exit 1
1000
+ chmod 600 "$META_TMP"
1001
+ mv "$META_TMP" "$JOB_DIR/meta.json"
1002
+
944
1003
  if [[ -n "$THREAD_NAME" ]]; then
945
1004
  printf 'omnilane: thread %s turn %s (%s session %s, %s)\n' \
946
1005
  "$THREAD_NAME" "$THREAD_TURN" "$VENDOR" "$THREAD_ID" "$THREAD_MODE"
@@ -948,6 +1007,9 @@ fi
948
1007
 
949
1008
  secure_job_files() {
950
1009
  find "$JOB_DIR" -type f -exec chmod 600 {} +
1010
+ if [[ -f "$JOB_WORKER" && ! -L "$JOB_WORKER" ]]; then
1011
+ chmod 400 "$JOB_WORKER"
1012
+ fi
951
1013
  }
952
1014
 
953
1015
  sanitize_utf8() {
@@ -1023,6 +1085,9 @@ write_completion_record() {
1023
1085
  "$rc" "$(json_escape "$finished")" "$(json_escape "$tail_value")" \
1024
1086
  > "$tmp" || write_rc=$?
1025
1087
  fi
1088
+ if [[ "$write_rc" -eq 0 ]] && ! json_file_round_trip_valid "$tmp"; then
1089
+ write_rc=1
1090
+ fi
1026
1091
  if [[ "$write_rc" -eq 0 ]]; then
1027
1092
  chmod 600 "$tmp" || write_rc=$?
1028
1093
  fi
@@ -1047,7 +1112,7 @@ finish_job() {
1047
1112
  secure_job_files
1048
1113
  (umask 077; printf '%s\n' "$rc" > "$JOB_DIR/exit")
1049
1114
  if [[ "${OMNILANE_INBOX:-1}" != "0" ]]; then
1050
- write_completion_record "$rc" >/dev/null 2>&1 || true
1115
+ write_completion_record "$rc" >/dev/null || true
1051
1116
  fi
1052
1117
  FINISHED_RC="$rc"
1053
1118
  }
@@ -1057,11 +1122,15 @@ run_job() {
1057
1122
  write_current_pid_file "$JOB_DIR/pid"
1058
1123
  set +e
1059
1124
  if [[ -n "$JOB_TIMEOUT" ]]; then
1060
- OMNILANE_JOB_SUPERVISED=1 perl "$JOB_SUPERVISOR" "$JOB_TIMEOUT" \
1061
- "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1125
+ OMNILANE_JOB_WORKER_REPO="$OMNILANE_REPO" \
1126
+ OMNILANE_JOB_WORKER_EXPECTED_SHA256="$JOB_WORKER_SHA256" \
1127
+ OMNILANE_JOB_SUPERVISED=1 perl "$JOB_SUPERVISOR" "$JOB_TIMEOUT" \
1128
+ "$JOB_WORKER_BASH" "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1062
1129
  "$JOB_DIR/task.txt" "$JOB_DIR/out.txt"
1063
1130
  else
1064
- "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1131
+ OMNILANE_JOB_WORKER_REPO="$OMNILANE_REPO" \
1132
+ OMNILANE_JOB_WORKER_EXPECTED_SHA256="$JOB_WORKER_SHA256" \
1133
+ "$JOB_WORKER_BASH" "$JOB_WORKER" "$VENDOR" "$MODE" "$WORKDIR" "$MODEL" "$EFFORT" \
1065
1134
  "$JOB_DIR/task.txt" "$JOB_DIR/out.txt"
1066
1135
  fi
1067
1136
  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"
package/scripts/jobs.sh CHANGED
@@ -137,11 +137,15 @@ load_job_vendor() {
137
137
  }
138
138
 
139
139
  require_live_job() {
140
+ local action="${1:-}"
140
141
  load_job_vendor
141
142
  live_vendor_capable "$JOB_VENDOR" ||
142
143
  die 1 "job vendor '$JOB_VENDOR' is not live-capable (live vendors: $(live_capable_vendors)); it runs in single-shot mode"
143
144
  [[ "$JOB_SESSION_MODE" == "live" ]] ||
144
145
  die 1 "job vendor '$JOB_VENDOR' was dispatched in single-shot mode"
146
+ if [[ "$action" == "send" && "$JOB_VENDOR" == "grok" ]]; then
147
+ echo "note: Grok send cancels an active turn before starting the new prompt; conversation context survives, but in-progress work is lost"
148
+ fi
145
149
  }
146
150
 
147
151
  wait_for_live_ready() {
@@ -194,7 +198,7 @@ parse_stats_metadata() {
194
198
  parse_audit_metadata() {
195
199
  local value="$1" current_re legacy_re
196
200
  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\"\\}$"
201
+ 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
202
  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
203
  [[ "$value" =~ $current_re || "$value" =~ $legacy_re ]]
200
204
  }
@@ -405,7 +409,7 @@ case "${1:-}" in
405
409
  send)
406
410
  [[ "$JSON_MODE" -eq 0 && $# -eq 3 ]] || usage
407
411
  select_job "$2"
408
- require_live_job
412
+ require_live_job send
409
413
  wait_for_live_ready
410
414
  if [[ "${FOREMAN_SESSION+x}" == "x" ]]; then
411
415
  foreman_value="$FOREMAN_SESSION"
@@ -37,6 +37,51 @@ json_escape() {
37
37
  printf '%s' "$out"
38
38
  }
39
39
 
40
+ json_file_round_trip_valid() {
41
+ local path="$1"
42
+ local valid=1
43
+ if [[ ! -f "$path" || -L "$path" ]]; then
44
+ valid=0
45
+ elif command -v python3 >/dev/null 2>&1; then
46
+ python3 - "$path" <<'PY' >/dev/null 2>&1 || valid=0
47
+ import json
48
+ import pathlib
49
+ import sys
50
+
51
+ value = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
52
+ json.dumps(value, ensure_ascii=False, separators=(",", ":"))
53
+ PY
54
+ elif command -v perl >/dev/null 2>&1; then
55
+ perl -MJSON::PP -e '
56
+ use strict;
57
+ use warnings;
58
+ my ($path) = @ARGV;
59
+ open my $fh, "<", $path or die $!;
60
+ local $/;
61
+ my $value = decode_json(<$fh>);
62
+ JSON::PP->new->canonical->encode($value);
63
+ ' "$path" >/dev/null 2>&1 || valid=0
64
+ else
65
+ valid=0
66
+ fi
67
+ if [[ "$valid" -ne 1 ]]; then
68
+ printf 'omnilane: JSON round-trip validation failed for %s\n' "$path" >&2
69
+ return 1
70
+ fi
71
+ }
72
+
73
+ file_sha256() {
74
+ local path="$1"
75
+ if command -v sha256sum >/dev/null 2>&1; then
76
+ sha256sum "$path" | awk '{print $1}'
77
+ elif command -v shasum >/dev/null 2>&1; then
78
+ shasum -a 256 "$path" | awk '{print $1}'
79
+ else
80
+ printf 'omnilane: no sha256sum/shasum available for worker provenance\n' >&2
81
+ return 1
82
+ fi
83
+ }
84
+
40
85
  resolve_timeout_cmd() {
41
86
  if command -v timeout &>/dev/null; then echo "timeout";
42
87
  elif command -v gtimeout &>/dev/null; then echo "gtimeout";
@@ -456,7 +501,8 @@ acquire_cwd_lock() { # vendor, workdir — the lock keys on the TARGET dir, not
456
501
  empty_since=-1
457
502
  fi
458
503
  if [[ "$waited" -ge "$lock_timeout" ]]; then
459
- echo "omnilane: lock timeout for $vendor in $dir" >&2; exit 87
504
+ echo "omnilane: lock timeout for $vendor in $dir; advise/read-only tasks may use a different --workdir to avoid this lock" >&2
505
+ exit 87
460
506
  fi
461
507
  sleep 2; waited=$((waited + 2))
462
508
  done