omnilane 0.42.6 → 0.42.8
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -1
- package/README.ja.md +48 -5
- package/README.ko.md +47 -5
- package/README.md +55 -6
- package/README.zh-CN.md +41 -6
- package/README.zh-TW.md +41 -6
- package/VERSION +1 -1
- package/bin/omnilane +8 -0
- package/completions/_omnilane +1 -1
- package/completions/omnilane.bash +1 -1
- package/completions/omnilane.fish +1 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/dispatch.sh +11 -0
- package/scripts/lib/aa_policy.py +4 -1
- package/scripts/lib/aa_retry.py +3 -1
- package/scripts/lib/caller_identity.py +521 -0
- package/scripts/runners/run-codex.sh +7 -1
- package/skills/omnilane/SKILL.md +42 -14
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "One routing table, native-first or CLI delegation, supervised completion workflows.",
|
|
9
|
-
"version": "0.42.
|
|
9
|
+
"version": "0.42.8"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "omnilane",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Route each subtask to the model that fits, then delegate through a compatible caller-owned native agent or vendor CLI.",
|
|
16
|
-
"version": "0.42.
|
|
16
|
+
"version": "0.42.8",
|
|
17
17
|
"category": "development",
|
|
18
18
|
"keywords": [
|
|
19
19
|
"routing",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnilane",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.8",
|
|
4
4
|
"description": "One routing table, every harness: classify subtasks into lanes and delegate through compatible caller-owned native agents or vendor CLIs with exact-AA downward policy and supervised jobs.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Seraphim0916"
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,83 @@ semantic version tags.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.42.8] - 2026-09-12
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- When ancestor inspection fails under `CODEX_SANDBOX=seatbelt`, `whoami` now explains that Codex blocked process inspection and that dispatch must be rerun outside the sandbox because omnilane also needs `~/.omnilane` writes and network access; non-sandbox refusals are unchanged.
|
|
14
|
+
|
|
15
|
+
- Codex `app-server` caller identity always comes from the latest host-written
|
|
16
|
+
`turn_context`, never its startup defaults. Other Codex launches use rollout
|
|
17
|
+
evidence only when no explicit model was selected (profiles are not selectors).
|
|
18
|
+
- Bind `CODEX_THREAD_ID` in the current process to the codex direct child's
|
|
19
|
+
initial environment. macOS reads argv and environment separately through
|
|
20
|
+
`KERN_PROCARGS2`; Linux reads `/proc`. Look the rollout up as
|
|
21
|
+
`rollout-*-<thread>.jsonl` and, for a resumed thread,
|
|
22
|
+
`rollout-*-<thread>_<session>.jsonl`, read the most recently written match, and
|
|
23
|
+
require a matching `session_meta.id`, non-empty model/effort/turn id, and no
|
|
24
|
+
later `task_complete`, its `turn_complete` read alias, or `turn_aborted`
|
|
25
|
+
carrying that same turn id. A refusal names both turn ids and how long ago the
|
|
26
|
+
rollout was last written. Missing, malformed or stale evidence refuses.
|
|
27
|
+
- Stream JSONL records and retain only identity metadata and event types;
|
|
28
|
+
diagnostics never include message content. `whoami` reports thread and turn ids.
|
|
29
|
+
- Accept Codex `-c model=...` / `--config` overrides, including attached/equals
|
|
30
|
+
forms and TOML strings (Python 3.11+); explicit `-m` / `--model` still wins
|
|
31
|
+
outside app-server. Other vendors and caller-context/inherited/human priority
|
|
32
|
+
remain unchanged. `OMNILANE_AA_CALLER_FROM_PROCESS=0` disables both readers.
|
|
33
|
+
- The test suite no longer inherits the dispatcher's AA environment. Run inside
|
|
34
|
+
an omnilane worker it kept the exported `OMNILANE_AA_*`, so the authorizer
|
|
35
|
+
identity and the transport overlay hash pin outranked each fixture's own human
|
|
36
|
+
exemption and six tests failed for that reason alone.
|
|
37
|
+
|
|
38
|
+
### Verification
|
|
39
|
+
|
|
40
|
+
- Accepted on real Codex desktop threads on 2026-09-12: `whoami` and
|
|
41
|
+
`dispatch.sh --dry-run` both exited 0 in a fresh thread and in a resumed thread
|
|
42
|
+
whose original rollout had stopped the previous day, reporting
|
|
43
|
+
`codex/gpt-6-astra-xhigh (score 54)` and `"allowed":true` with no job created.
|
|
44
|
+
- Source contract verified earlier in five real turns with bundled app-server
|
|
45
|
+
0.153.4. 較新版本若延後寫檔,過期保護會讓它拒絕,而不是讀到舊的一輪。
|
|
46
|
+
- Metadata-only check of the two 0.154.0 worker rollouts: thread
|
|
47
|
+
`01a08f9e-6450-7f82-85e9-af534954b4ac` was absent from both active and archived
|
|
48
|
+
directories; `01a08fb4-fd1a-7811-a457-c21a4a6ff4d8` existed in active sessions,
|
|
49
|
+
created 2026-09-11 17:03:18 Asia/Taipei. Neither file's contents were opened.
|
|
50
|
+
|
|
51
|
+
## [0.42.7] - 2026-09-10
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `omnilane whoami` prints a caller-context file for the CLI it runs under. It
|
|
56
|
+
walks up the process tree to the nearest vendor CLI, reads the model and
|
|
57
|
+
effort that CLI was launched with, and maps them onto the one scored
|
|
58
|
+
configuration they select, or exits 3 with the reason: a missing `--effort`,
|
|
59
|
+
a model alias, or a Claude effort whose only scored row is non-reasoning. It
|
|
60
|
+
never guesses.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- A dispatch that carries no `--caller-context` and no human assertion reads
|
|
65
|
+
the caller's identity from its launching CLI. Model sessions outside the
|
|
66
|
+
omnilane checkout used to stop on `missing-caller-context` and hand the
|
|
67
|
+
question back to the operator — three did on 2026-09-08 and 2026-09-10 —
|
|
68
|
+
because the refusal offered a model two options it could not take, and the
|
|
69
|
+
instructions for a third lived where those sessions never looked.
|
|
70
|
+
- Launch flags are set by the harness rather than the model, so an identity
|
|
71
|
+
read from them is harder to overstate than a hand-written file, which the
|
|
72
|
+
gate checks for shape but not against what is running. Each session is held
|
|
73
|
+
to its own flags: the same model at `high` and at `max` gets 52 and 54.
|
|
74
|
+
- An explicit `--caller-context`, the context a worker inherits, and
|
|
75
|
+
`--operator-asserted-human` all take precedence.
|
|
76
|
+
`OMNILANE_AA_CALLER_FROM_PROCESS=0` restores the file-only contract.
|
|
77
|
+
- The `missing-caller-context` and retry refusals name `omnilane whoami`.
|
|
78
|
+
Retries still require an explicit current identity; they are not read
|
|
79
|
+
automatically.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- `omnilane --version` reported 0.42.5 in the 0.42.6 release, whose `VERSION`
|
|
84
|
+
file was not bumped with the package manifests.
|
|
85
|
+
|
|
9
86
|
## [0.42.6] - 2026-09-10
|
|
10
87
|
|
|
11
88
|
### Added
|
|
@@ -981,7 +1058,10 @@ work to the wrong model, and records the evidence behind the shipped defaults.
|
|
|
981
1058
|
- Initial shared routing table, cross-vendor dispatcher, runners, installer,
|
|
982
1059
|
and baseline lint fixes.
|
|
983
1060
|
|
|
984
|
-
[Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.42.
|
|
1061
|
+
[Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.42.8...HEAD
|
|
1062
|
+
[0.42.8]: https://github.com/Seraphim0916/omnilane/compare/v0.42.7...v0.42.8
|
|
1063
|
+
[0.42.7]: https://github.com/Seraphim0916/omnilane/compare/v0.42.6...v0.42.7
|
|
1064
|
+
[0.42.6]: https://github.com/Seraphim0916/omnilane/compare/v0.42.5...v0.42.6
|
|
985
1065
|
[0.42.5]: https://github.com/Seraphim0916/omnilane/compare/v0.42.4...v0.42.5
|
|
986
1066
|
[0.42.4]: https://github.com/Seraphim0916/omnilane/compare/v0.42.3...v0.42.4
|
|
987
1067
|
[0.42.3]: https://github.com/Seraphim0916/omnilane/compare/v0.42.2...v0.42.3
|
package/README.ja.md
CHANGED
|
@@ -71,8 +71,10 @@ omnilane route hardest-coding "auth トークン更新テストの不安定さ
|
|
|
71
71
|
> ゲートするため、「誰が依頼しているか」を必ず示す必要があります。端末の前にいる人間は
|
|
72
72
|
> `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` を一度設定するか、呼び出しごとに
|
|
73
73
|
> `--operator-asserted-human` を付けます。omnilane を動かすモデルは**自分でこれを主張
|
|
74
|
-
>
|
|
75
|
-
>
|
|
74
|
+
> できません**。モデルの識別情報は、それを起動した CLI のモデル・effort フラグから自動で
|
|
75
|
+
> 読み取られるため、通常のセッションは何も渡す必要がありません。`omnilane whoami` はその
|
|
76
|
+
> 識別情報を `--caller-context FILE` として出力します。主張も読み取れる識別情報も無い場合、
|
|
77
|
+
> ジョブ生成前に `missing-caller-context` で拒否されます。
|
|
76
78
|
|
|
77
79
|
> はじめての方は、まず `omnilane doctor` を実行してください。omnilane が今どのモデル CLI と
|
|
78
80
|
> API キーに接続できるかがわかり、実際に何が動くか把握できます。
|
|
@@ -530,10 +532,12 @@ work の別名ではありません。サービス管理など、work の境界
|
|
|
530
532
|
ください。その `transport-overlay` チェックが、問題はこのマシンの設定なのか
|
|
531
533
|
リクエストなのかをすぐに示します。
|
|
532
534
|
|
|
533
|
-
`missing-caller-context` —
|
|
535
|
+
`missing-caller-context` — 識別情報がゲートに届いていません。人間は
|
|
534
536
|
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` または `--operator-asserted-human` を
|
|
535
|
-
|
|
536
|
-
|
|
537
|
+
使います。モデルは通常何もしなくてよく、dispatch が起動元の CLI から識別情報を
|
|
538
|
+
読み取ります。読み取れない場合は `omnilane whoami` を実行してください。渡すべき
|
|
539
|
+
`--caller-context FILE` を出力するか、読み取れない理由(`--effort` 未指定、モデル
|
|
540
|
+
エイリアス、スコア行なし)を示します。モデルは人間向けの免除を自称してはいけません。
|
|
537
541
|
|
|
538
542
|
`runtime-mapping-unverified` — 識別情報は正しく、**ターゲット**にホストローカルの
|
|
539
543
|
リクエストセレクタの証明がありません。未プローブか、プローブが失敗しています。
|
|
@@ -573,6 +577,45 @@ work の別名ではありません。サービス管理など、work の境界
|
|
|
573
577
|
|
|
574
578
|
## 📜 リリース履歴
|
|
575
579
|
|
|
580
|
+
## v0.42.8 の新機能
|
|
581
|
+
|
|
582
|
+
- **Codex の現在ターンの身元。** `app-server` は起動時のモデル・強度を常に無視します。
|
|
583
|
+
それ以外の Codex は明示的な `-m` / `--model` / `-c model=...` / `--config` を維持し、
|
|
584
|
+
モデル指定がない場合(プロファイルのみを含む)は現在ターンの記録を使います。
|
|
585
|
+
TOML のモデル指定には Python 3.11 以降が必要です。
|
|
586
|
+
- **曖昧なら拒否。** 現在のプロセス環境と Codex 直下の子プロセスの初期環境に、同じ UUID 形式の
|
|
587
|
+
`CODEX_THREAD_ID` が必要です。記録は `$CODEX_HOME/sessions`(既定 `~/.codex`)から
|
|
588
|
+
`rollout-*-<スレッド>.jsonl`、再開後は `rollout-*-<スレッド>_<セッション>.jsonl` として探し、
|
|
589
|
+
最後に書き込まれたものを読み、`session_meta.id` が一致する必要があります。最後の `turn_context`
|
|
590
|
+
にはモデル・強度・ターン ID が必要です。その後に**同じターン ID** の `task_complete` /
|
|
591
|
+
`turn_complete`(読み取り時の別名)/ `turn_aborted` があれば古い身元として拒否し、
|
|
592
|
+
両方のターン ID と、その記録が最後に書かれてからの経過時間を示します。
|
|
593
|
+
- **Codex サンドボックスでの拒否。** 祖先検索は従来どおり先に実行します。`CODEX_SANDBOX=seatbelt`
|
|
594
|
+
で失敗した場合、`whoami` はプロセス検査、`~/.omnilane` への書き込み、ネットワーク接続に
|
|
595
|
+
サンドボックス外での再実行が必要だと案内します。
|
|
596
|
+
- **追跡可能性と互換性。** JSONL は逐次読み込み、メッセージ本文を診断に含めません。
|
|
597
|
+
`whoami` はスレッドとターン ID を表示します。設定既定値・モデル一覧・アーカイブ・他の会話は
|
|
598
|
+
代用しません。他社 CLI、明示・継承した身元、人間の宣言の優先順位は変更しません。
|
|
599
|
+
`OMNILANE_AA_CALLER_FROM_PROCESS=0` は両方の読み取りを無効にします。
|
|
600
|
+
- **テストの環境分離。** テストはディスパッチ元のワーカーから `OMNILANE_AA_*` を
|
|
601
|
+
引き継がなくなりました。ワーカー内で実行すると、承認者の身元と転送オーバーレイの
|
|
602
|
+
ハッシュ固定が各フィクスチャ自身の人間宣言を上書きし、6 件が失敗していました。
|
|
603
|
+
- **実機での受け入れ。** 2026-09-12 に Codex デスクトップで実測:新規スレッドと、
|
|
604
|
+
元の記録が前日で止まっていた再開スレッドの両方で `whoami` と
|
|
605
|
+
`dispatch.sh --dry-run` が終了コード 0、`codex/gpt-6-astra-xhigh (score 54)` と
|
|
606
|
+
`"allowed":true` を返し、ジョブは作成されませんでした。
|
|
607
|
+
更新は `npm i -g omnilane@0.42.8`。
|
|
608
|
+
|
|
609
|
+
## v0.42.7 の新機能
|
|
610
|
+
|
|
611
|
+
- **モデルのセッションは識別ファイルなしでディスパッチできます。** `--caller-context` が無い場合、dispatch はプロセスツリーを遡って最も近いベンダー CLI を見つけ、その起動時のモデルと effort を読み取ります。これまで omnilane のチェックアウト外のセッションは `missing-caller-context` で止まり、判断をオペレーターに差し戻していました(2026-09-08 と 2026-09-10 に 3 件)。
|
|
612
|
+
- **`omnilane whoami`** はその識別情報を caller-context ファイルとして出力し、読み取れない場合は理由(`--effort` 未指定、モデルエイリアス、Claude のその effort に non-reasoning 行しか無い)を示します。推測はしません。
|
|
613
|
+
- **手書きファイルより偽りにくい。** ゲートは caller-context ファイルの形式しか確認せず、実際に動いているモデルと一致するかは見ていません。起動フラグはモデルではなくハーネスが設定し、セッションごとに判定されます。同じモデルでも `high` と `max` なら上限は 52 と 54 です。
|
|
614
|
+
- **明示指定が優先。** `--caller-context` ファイル、ワーカーが継承する環境、`--operator-asserted-human` はいずれも自動読み取りより優先します。`OMNILANE_AA_CALLER_FROM_PROCESS=0` でファイルのみの契約に戻せます。
|
|
615
|
+
- **拒否メッセージが出口を示します。** `missing-caller-context` と再試行の拒否は `omnilane whoami` を案内します。
|
|
616
|
+
- **`omnilane --version` が正しくなりました。** 0.42.6 では `VERSION` が 0.42.5 のままでした。
|
|
617
|
+
- **アップグレード。** npm 公開後は `npm i -g omnilane@0.42.7` を実行してください。
|
|
618
|
+
|
|
576
619
|
## v0.42.6 の新機能
|
|
577
620
|
|
|
578
621
|
- **「検証済み」が、どう検証されたかを示すようになりました。** 各 overlay マッピングは `evidence_tier` を持ちます。`billed-model` はプロバイダー自身が課金対象のモデル名を返した場合(claude、grok)、`client-echo` は CLI が自ら送信したモデルを記録した場合(codex、agy)、`selector-only` は CLI がセレクターを受け付けただけの場合です。`client-echo` は CLI が控えた注文書、`billed-model` はプロバイダーが発行した領収書です。
|
package/README.ko.md
CHANGED
|
@@ -70,8 +70,10 @@ omnilane route hardest-coding "간헐적으로 실패하는 auth 토큰 갱신
|
|
|
70
70
|
> 게이트하므로, 디스패치는 «누가 요청하는지»를 반드시 밝혀야 합니다. 터미널 앞의 사람은
|
|
71
71
|
> `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 을 한 번 설정하거나 호출마다
|
|
72
72
|
> `--operator-asserted-human` 을 붙입니다. omnilane 을 구동하는 모델은 **스스로 이를 주장할
|
|
73
|
-
> 수
|
|
74
|
-
>
|
|
73
|
+
> 수 없습니다**. 모델의 신원은 그것을 실행한 CLI 의 모델·effort 플래그에서 자동으로 읽히므로,
|
|
74
|
+
> 일반 세션은 아무것도 전달할 필요가 없습니다. `omnilane whoami` 는 그 신원을
|
|
75
|
+
> `--caller-context FILE` 로 출력합니다. 주장도 읽을 수 있는 신원도 없으면 잡 생성 전에
|
|
76
|
+
> `missing-caller-context` 로 거부됩니다.
|
|
75
77
|
|
|
76
78
|
> 처음이신가요? 먼저 `omnilane doctor` 를 실행하세요. omnilane 이 지금 어떤 모델 CLI 와
|
|
77
79
|
> API 키에 접근할 수 있는지 알려 주어, 실제로 무엇이 실행될지 파악할 수 있습니다.
|
|
@@ -516,10 +518,12 @@ work 는 지정한 디렉터리 안의 변경만 허용하며 모델 연결은
|
|
|
516
518
|
`transport-overlay` 검사가 문제의 원인이 이 머신의 설정인지 요청인지 바로
|
|
517
519
|
알려줍니다.
|
|
518
520
|
|
|
519
|
-
`missing-caller-context` —
|
|
521
|
+
`missing-caller-context` — 게이트에 신원이 전달되지 않았습니다. 사람은
|
|
520
522
|
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 또는 `--operator-asserted-human`을
|
|
521
|
-
사용합니다.
|
|
522
|
-
|
|
523
|
+
사용합니다. 모델은 보통 아무것도 할 필요가 없으며, dispatch 가 실행한 CLI 에서 신원을
|
|
524
|
+
읽습니다. 읽지 못하면 `omnilane whoami` 를 실행하세요. 전달할 `--caller-context FILE`
|
|
525
|
+
을 출력하거나, 읽지 못한 정확한 이유(`--effort` 누락, 모델 별칭, 점수 행 없음)를 알려줍니다.
|
|
526
|
+
모델은 사람용 면제를 스스로 주장해서는 안 됩니다.
|
|
523
527
|
|
|
524
528
|
`runtime-mapping-unverified` — 신원은 정상이지만 **대상**에 검증된 호스트 로컬
|
|
525
529
|
요청 셀렉터가 없습니다. 프로브를 한 적이 없거나 프로브가 실패한 경우입니다.
|
|
@@ -558,6 +562,44 @@ doctor가 파일과 벤더를 지목하며, 재서명 절차는 디스패치 스
|
|
|
558
562
|
|
|
559
563
|
## 📜 릴리스 기록
|
|
560
564
|
|
|
565
|
+
## v0.42.8 새 기능
|
|
566
|
+
|
|
567
|
+
- **Codex 현재 턴 신원.** `app-server`는 시작 시 모델·강도 기본값을 항상 무시합니다.
|
|
568
|
+
다른 Codex 실행은 명시적인 `-m` / `--model` / `-c model=...` / `--config`를 유지하며,
|
|
569
|
+
모델 지정이 없으면(프로필만 지정한 경우 포함) 현재 턴 기록을 사용합니다.
|
|
570
|
+
TOML 모델 지정에는 Python 3.11 이상이 필요합니다.
|
|
571
|
+
- **불확실하면 거부.** 현재 프로세스 환경과 Codex의 직계 자식 프로세스 초기 환경에 같은 UUID 형식의
|
|
572
|
+
`CODEX_THREAD_ID`가 있어야 합니다. 기록은 `$CODEX_HOME/sessions`(기본 `~/.codex`)에서
|
|
573
|
+
`rollout-*-<스레드>.jsonl`로, 재개한 뒤에는 `rollout-*-<스레드>_<세션>.jsonl`로 찾으며,
|
|
574
|
+
가장 최근에 기록된 파일을 읽고 `session_meta.id`가 일치해야 합니다. 마지막 `turn_context`에는
|
|
575
|
+
모델·강도·턴 ID가 필요하며, 그 뒤에 **같은 턴 ID**의 `task_complete` / `turn_complete`(읽기 별칭) /
|
|
576
|
+
`turn_aborted`가 있으면 오래된 신원으로 거부하고, 두 턴 ID와 그 기록이 마지막으로 기록된 뒤
|
|
577
|
+
지난 시간을 함께 알려 줍니다.
|
|
578
|
+
- **Codex 샌드박스 거부.** 조상 조회는 기존처럼 먼저 실행합니다. `CODEX_SANDBOX=seatbelt`에서
|
|
579
|
+
실패하면 `whoami`는 프로세스 조회, `~/.omnilane` 쓰기 및 네트워크 연결을 위해 샌드박스
|
|
580
|
+
밖에서 명령을 다시 실행해야 한다고 안내합니다.
|
|
581
|
+
- **추적성과 호환성.** JSONL을 순차적으로 읽고 메시지 본문은 진단에 포함하지 않습니다.
|
|
582
|
+
`whoami`는 스레드와 턴 ID를 표시합니다. 설정 기본값·모델 목록·보관 기록·다른 대화로
|
|
583
|
+
추정하지 않습니다. 다른 공급자, 명시적·상속된 신원과 사람 선언의 우선순위는 유지합니다.
|
|
584
|
+
`OMNILANE_AA_CALLER_FROM_PROCESS=0`은 두 읽기 경로를 모두 끕니다.
|
|
585
|
+
- **테스트 환경 격리.** 테스트는 더 이상 디스패치 워커가 내보낸 `OMNILANE_AA_*`를
|
|
586
|
+
물려받지 않습니다. 워커 안에서 실행하면 승인자 신원과 전송 오버레이 해시 고정이 각
|
|
587
|
+
픽스처의 사람 선언을 덮어써 6개가 실패했습니다.
|
|
588
|
+
- **실제 환경 검증.** 2026-09-12 Codex 데스크톱에서 확인: 새 스레드와, 원래 기록이 전날
|
|
589
|
+
멈춰 있던 재개 스레드 모두에서 `whoami`와 `dispatch.sh --dry-run`이 종료 코드 0으로
|
|
590
|
+
`codex/gpt-6-astra-xhigh (score 54)`와 `"allowed":true`를 보고했고 작업은 생성되지
|
|
591
|
+
않았습니다. 업데이트: `npm i -g omnilane@0.42.8`.
|
|
592
|
+
|
|
593
|
+
## v0.42.7 새 기능
|
|
594
|
+
|
|
595
|
+
- **모델 세션은 신원 파일 없이 디스패치할 수 있습니다.** `--caller-context` 가 없으면 dispatch 가 프로세스 트리를 거슬러 올라가 가장 가까운 벤더 CLI 를 찾고, 그 CLI 가 실행될 때의 모델과 effort 를 읽습니다. 지금까지 omnilane 체크아웃 밖의 세션은 `missing-caller-context` 에서 멈추고 판단을 운영자에게 되돌렸습니다(2026-09-08, 2026-09-10 에 3건).
|
|
596
|
+
- **`omnilane whoami`** 는 그 신원을 caller-context 파일로 출력하고, 읽지 못하면 정확한 이유(`--effort` 누락, 모델 별칭, 해당 effort 에 Claude 의 non-reasoning 행만 있음)를 알려줍니다. 추측하지 않습니다.
|
|
597
|
+
- **손으로 쓴 파일보다 속이기 어렵습니다.** 게이트는 caller-context 파일의 형식만 확인하고 실제로 실행 중인 모델과 일치하는지는 보지 않습니다. 실행 플래그는 모델이 아니라 하네스가 설정하며, 세션마다 따로 판정됩니다. 같은 모델이라도 `high` 와 `max` 면 상한은 52 와 54 입니다.
|
|
598
|
+
- **명시 지정이 우선합니다.** `--caller-context` 파일, 워커가 물려받는 환경, `--operator-asserted-human` 은 모두 자동 읽기보다 우선합니다. `OMNILANE_AA_CALLER_FROM_PROCESS=0` 으로 파일 전용 계약으로 되돌릴 수 있습니다.
|
|
599
|
+
- **거부 메시지가 해결책을 알려줍니다.** `missing-caller-context` 와 재시도 거부는 `omnilane whoami` 를 안내합니다.
|
|
600
|
+
- **`omnilane --version` 이 다시 정확해졌습니다.** 0.42.6 에서는 `VERSION` 이 0.42.5 로 남아 있었습니다.
|
|
601
|
+
- **업그레이드.** npm 게시 후 `npm i -g omnilane@0.42.7`를 실행하세요.
|
|
602
|
+
|
|
561
603
|
## v0.42.6 새 기능
|
|
562
604
|
|
|
563
605
|
- **"검증됨"이 어떻게 검증했는지도 알려줍니다.** 각 overlay 매핑은 `evidence_tier`를 가집니다. `billed-model`은 공급자가 과금한 모델을 직접 밝힌 경우(claude, grok), `client-echo`는 CLI가 자신이 보낸 모델을 기록한 경우(codex, agy), `selector-only`는 CLI가 셀렉터를 받아들이기만 한 경우입니다. `client-echo`는 CLI가 적어 둔 주문서이고, `billed-model`은 공급자가 발행한 영수증입니다.
|
package/README.md
CHANGED
|
@@ -71,8 +71,11 @@ omnilane route hardest-coding "fix the flaky auth token refresh"
|
|
|
71
71
|
> capability score, so a dispatch has to say who is asking. A human at a terminal
|
|
72
72
|
> asserts that once with `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1`, or per call with
|
|
73
73
|
> `--operator-asserted-human`. A model driving omnilane cannot assert it for
|
|
74
|
-
> itself
|
|
75
|
-
> effort
|
|
74
|
+
> itself. Its identity is read from the nearest launching CLI: explicit model and
|
|
75
|
+
> effort flags, or for Codex app-server/no-model launches, a bound current-turn
|
|
76
|
+
> rollout (never app-server startup defaults). An ordinary session passes nothing,
|
|
77
|
+
> and `omnilane whoami` prints that identity as a `--caller-context FILE`. With
|
|
78
|
+
> neither an assertion nor a readable identity, the dispatch is refused with
|
|
76
79
|
> `missing-caller-context` before any job is created.
|
|
77
80
|
|
|
78
81
|
> New to this? Run `omnilane doctor` first — it tells you which model CLIs and
|
|
@@ -625,10 +628,14 @@ Three codes, three different fixes. Run `omnilane doctor` first — its
|
|
|
625
628
|
`transport-overlay` check tells you immediately whether the problem is your
|
|
626
629
|
machine's configuration or your request.
|
|
627
630
|
|
|
628
|
-
`missing-caller-context` —
|
|
629
|
-
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` or `--operator-asserted-human
|
|
630
|
-
|
|
631
|
-
|
|
631
|
+
`missing-caller-context` — no identity reached the gate. A human adds
|
|
632
|
+
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` or `--operator-asserted-human`. A model
|
|
633
|
+
normally needs nothing: dispatch reads its identity from the nearest launching
|
|
634
|
+
CLI, using a bound current-turn rollout for Codex app-server/no-model launches.
|
|
635
|
+
When that fails, run `omnilane whoami` — it either prints a
|
|
636
|
+
`--caller-context FILE` to pass, or says exactly why it cannot (a missing
|
|
637
|
+
`--effort`, a model alias, no scored row). A model must not assert the human
|
|
638
|
+
exemption for itself.
|
|
632
639
|
|
|
633
640
|
`runtime-mapping-unverified` — your identity is fine, but the *target* has no
|
|
634
641
|
proven host-local request selector. Either it was never probed, or its probe
|
|
@@ -699,6 +706,48 @@ working notes, including per-benchmark caveats, live in
|
|
|
699
706
|
|
|
700
707
|
## 📜 Release history
|
|
701
708
|
|
|
709
|
+
## What's new in v0.42.8
|
|
710
|
+
|
|
711
|
+
- **Codex current-turn identity.** `app-server` always ignores startup model and
|
|
712
|
+
effort defaults. Other Codex launches keep explicit `-m` / `--model` or
|
|
713
|
+
`-c model=...` / `--config` selectors (TOML model overrides require Python 3.11+);
|
|
714
|
+
no model, including profile-only launches, uses the rollout path.
|
|
715
|
+
- **Bound, fail-closed evidence.** The current process environment and the codex
|
|
716
|
+
direct-child initial environment must carry the same UUID-shaped
|
|
717
|
+
`CODEX_THREAD_ID`. The rollout is looked up under `$CODEX_HOME/sessions`
|
|
718
|
+
(default `~/.codex`) as `rollout-*-<thread>.jsonl` and, after a resume,
|
|
719
|
+
`rollout-*-<thread>_<session>.jsonl`; the most recently written match is read
|
|
720
|
+
and its `session_meta.id` must match. Its latest `turn_context` needs model,
|
|
721
|
+
effort and turn id; a later `task_complete`, `turn_complete` (read alias) or
|
|
722
|
+
`turn_aborted` carrying that same turn id refuses stale identity, and the
|
|
723
|
+
refusal names both turn ids and how long ago the rollout was written. No config,
|
|
724
|
+
model-list, archive or other-session fallback; JSONL is streamed and message
|
|
725
|
+
content is never included in diagnostics. `whoami` reports thread and turn ids.
|
|
726
|
+
- **Codex sandbox refusal.** Ancestor lookup still runs first. If it fails under
|
|
727
|
+
`CODEX_SANDBOX=seatbelt`, `whoami` explains that process inspection,
|
|
728
|
+
`~/.omnilane` writes and networking require rerunning outside the sandbox.
|
|
729
|
+
- **Hermetic tests.** The suite no longer inherits `OMNILANE_AA_*` from a
|
|
730
|
+
dispatching worker, where the authorizer identity and the overlay hash pin used
|
|
731
|
+
to outrank each fixture's own human exemption and fail six tests.
|
|
732
|
+
- **Compatibility.** Other vendors, explicit/inherited caller identity and human
|
|
733
|
+
assertion precedence are unchanged. `OMNILANE_AA_CALLER_FROM_PROCESS=0` disables
|
|
734
|
+
both argv and rollout reading.
|
|
735
|
+
- **Accepted on a real desktop thread.** On 2026-09-12 `whoami` and
|
|
736
|
+
`dispatch.sh --dry-run` both exited 0 from the Codex desktop app — in a fresh
|
|
737
|
+
thread and in a resumed one whose original rollout had stopped the day before,
|
|
738
|
+
reporting `codex/gpt-6-astra-xhigh (score 54)` and `"allowed":true`.
|
|
739
|
+
- **Upgrade.** Run `npm i -g omnilane@0.42.8`.
|
|
740
|
+
|
|
741
|
+
## What's new in v0.42.7
|
|
742
|
+
|
|
743
|
+
- **A model session no longer needs an identity file to dispatch.** When no `--caller-context` is given, dispatch walks up the process tree to the nearest vendor CLI and reads the model and effort it was launched with. Sessions outside the omnilane checkout used to stop on `missing-caller-context` and hand the question back to the operator; three did so on 2026-09-08 and 2026-09-10.
|
|
744
|
+
- **`omnilane whoami`** prints that identity as a caller-context file, or says exactly why it cannot — a missing `--effort`, a model alias, or a Claude effort whose only scored row is non-reasoning. It never guesses.
|
|
745
|
+
- **Harder to overstate than a hand-written file.** The gate checks a caller-context file's shape, not whether it matches the model actually running. Launch flags are set by the harness, not the model, and each session is held to its own: the same model at `high` and at `max` gets ceilings 52 and 54.
|
|
746
|
+
- **Explicit still wins.** A `--caller-context` file, the context a worker inherits, and `--operator-asserted-human` all take precedence. `OMNILANE_AA_CALLER_FROM_PROCESS=0` restores the file-only contract.
|
|
747
|
+
- **The refusal names the way out.** `missing-caller-context` and the retry refusal now point to `omnilane whoami` instead of offering a model two options it cannot take.
|
|
748
|
+
- **`omnilane --version` is right again.** The 0.42.6 release left `VERSION` at 0.42.5.
|
|
749
|
+
- **Upgrade.** After npm publication, run `npm i -g omnilane@0.42.7`. Existing repo-symlink installations can update their checkout and verify `omnilane --version` without rerunning installation.
|
|
750
|
+
|
|
702
751
|
## What's new in v0.42.6
|
|
703
752
|
|
|
704
753
|
- **A verified mapping now says how it was verified.** Each overlay mapping carries an `evidence_tier`: `billed-model` when the provider named the model it charged for (Claude, grok), `client-echo` when the CLI recorded the model it asked for (codex, agy), `selector-only` when the CLI accepted the selector and said nothing more. `client-echo` is the CLI's copy of your order; `billed-model` is the provider's receipt.
|
package/README.zh-CN.md
CHANGED
|
@@ -65,8 +65,9 @@ omnilane route hardest-coding "修掉会间歇失败的 auth token 更新测试"
|
|
|
65
65
|
> **那个 export 是做什么的?** omnilane 会用调用者自己的能力分数来把关每一次派工,
|
|
66
66
|
> 所以派工必须表明「是谁在问」。人类在终端前只要设一次
|
|
67
67
|
> `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1`,或每次带 `--operator-asserted-human`。
|
|
68
|
-
> 模型驱动 omnilane
|
|
69
|
-
>
|
|
68
|
+
> 模型驱动 omnilane 时**不能替自己主张**这个标志。它的身份会从启动它的 CLI 标志
|
|
69
|
+
> (模型与强度)自动读取,一般 session 什么都不用带;`omnilane whoami` 会把这个身份
|
|
70
|
+
> 打印成 `--caller-context FILE`。既没主张、又读不到身份的话,派工会在创建作业前就被
|
|
70
71
|
> `missing-caller-context` 拒绝。
|
|
71
72
|
|
|
72
73
|
> 第一次用?先跑 `omnilane doctor`——它会告诉你 omnilane 现在能接到哪些模型 CLI 与
|
|
@@ -489,10 +490,11 @@ scripts/dispatch.sh --dry-run hardest-coding "…" # 完整解析后的计划,
|
|
|
489
490
|
三个代码,三种不同的修法。先运行 `omnilane doctor`——它的 `transport-overlay`
|
|
490
491
|
检查会直接告诉你问题出在本机配置还是你的请求。
|
|
491
492
|
|
|
492
|
-
`missing-caller-context
|
|
493
|
-
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 或 `--operator-asserted-human
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
`missing-caller-context`——没有身份送到闸门。真人加上
|
|
494
|
+
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 或 `--operator-asserted-human`。
|
|
495
|
+
模型通常什么都不用做:dispatch 会从启动它的 CLI 读取身份。读不到时运行
|
|
496
|
+
`omnilane whoami`,它会打印可以带的 `--caller-context FILE`,或说明读不到的确切原因
|
|
497
|
+
(缺 `--effort`、模型别名、查无评分行)。模型不得替自己主张真人豁免。
|
|
496
498
|
|
|
497
499
|
`runtime-mapping-unverified`——你的身份没问题,但**目标**没有已验证的本机请求
|
|
498
500
|
选择器。可能从未探测过,也可能探测失败;`omnilane doctor` 会报告这类配置的数量,
|
|
@@ -554,6 +556,39 @@ codex 记在 session rollout,agy 写进 `cli.log`。这是 CLI 自己抄的订
|
|
|
554
556
|
|
|
555
557
|
## 📜 版本历程
|
|
556
558
|
|
|
559
|
+
## v0.42.8 新功能
|
|
560
|
+
|
|
561
|
+
- **Codex 当前轮次身份。** `app-server` 始终忽略启动参数中的模型和强度默认值。
|
|
562
|
+
其他 Codex 启动保留明确的 `-m` / `--model` / `-c model=...` / `--config`;
|
|
563
|
+
未指定模型(包括只指定 profile)时读取当前轮次记录。TOML 模型覆盖需要 Python 3.11+。
|
|
564
|
+
- **证据不明就拒绝。** 本进程的当前环境与 Codex 直属子进程的初始环境必须有相同、符合 UUID 格式的
|
|
565
|
+
`CODEX_THREAD_ID`。记录文件在 `$CODEX_HOME/sessions`(默认 `~/.codex`)下按
|
|
566
|
+
`rollout-*-<对话串>.jsonl` 查找,对话串续用后还会有 `rollout-*-<对话串>_<会话>.jsonl`;
|
|
567
|
+
取最后写入的那一个,`session_meta.id` 必须一致。最后一个 `turn_context` 必须包含模型、强度和轮次 ID;
|
|
568
|
+
后面若出现**同一个轮次 ID** 的 `task_complete` / `turn_complete`(读取别名)/ `turn_aborted`,
|
|
569
|
+
就拒绝使用过期身份,并在消息里写出两个轮次 ID,以及该记录多久没有被写入。
|
|
570
|
+
- **Codex 沙箱拒绝。** 仍会先查询祖先进程;若在 `CODEX_SANDBOX=seatbelt` 下查询失败,
|
|
571
|
+
`whoami` 会说明进程查询、写入 `~/.omnilane` 和联网都需要在沙箱外重新执行命令。
|
|
572
|
+
- **隐私与兼容。** 逐行读取 JSONL,诊断不含消息正文;`whoami` 会报告对话串和轮次 ID。
|
|
573
|
+
不从配置默认值、模型列表、归档或其他对话推断。其他供应商、明确或继承的身份、人类声明
|
|
574
|
+
的优先级不变;`OMNILANE_AA_CALLER_FROM_PROCESS=0` 同时关闭两种读取路径。
|
|
575
|
+
- **测试自己清环境。** 测试不再沿用派工工人带进来的 `OMNILANE_AA_*`;在工人里运行时,
|
|
576
|
+
授权者身份与传输对照表的哈希锁定会盖掉各夹具自己的人类豁免,导致 6 个测试失败。
|
|
577
|
+
- **实机验收。** 2026-09-12 在 Codex 桌面版实测:新建对话串与续用对话串(原始记录文件
|
|
578
|
+
前一天就停止写入)各跑一次,`whoami` 与 `dispatch.sh --dry-run` 都退出 0,读出
|
|
579
|
+
`codex/gpt-6-astra-xhigh (score 54)`、`"allowed":true`,且没有创建任何任务。
|
|
580
|
+
升级:`npm i -g omnilane@0.42.8`。
|
|
581
|
+
|
|
582
|
+
## v0.42.7 新功能
|
|
583
|
+
|
|
584
|
+
- **模型 session 派工不再需要身份文件。** 没给 `--caller-context` 时,dispatch 会沿进程树往上找到最近的厂商 CLI,读取它启动时带的模型与强度。以前不在 omnilane 目录下的 session 会卡在 `missing-caller-context`,把问题丢回给操作者——2026-09-08 与 2026-09-10 就有三个 session 这样停下来。
|
|
585
|
+
- **`omnilane whoami`** 会把这个身份打印成 caller-context 文件,读不到时说明确切原因(缺 `--effort`、模型别名、claude 该强度只剩 non-reasoning 行),绝不猜测。
|
|
586
|
+
- **比手写的文件更难作假。** 闸门只检查身份文件的格式,不核对它与实际运行的模型是否一致。启动标志由 harness 设置,模型改不了,而且每个 session 各算各的:同一模型分别开 `high` 和 `max`,上限就是 52 和 54。
|
|
587
|
+
- **明确指定仍然优先。** `--caller-context` 文件、worker 继承的环境、`--operator-asserted-human` 都优先于自动读取。`OMNILANE_AA_CALLER_FROM_PROCESS=0` 可恢复「只认文件」的规则。
|
|
588
|
+
- **被拒时会告诉你出路。** `missing-caller-context` 和重试被拒的消息都改为指向 `omnilane whoami`。
|
|
589
|
+
- **`omnilane --version` 恢复正确。** 0.42.6 发版时漏改了 `VERSION`,会报告 0.42.5。
|
|
590
|
+
- **升级。** npm 发布后运行 `npm i -g omnilane@0.42.7`。既有的 repo symlink 安装更新检出后确认 `omnilane --version` 即可。
|
|
591
|
+
|
|
557
592
|
## v0.42.6 新功能
|
|
558
593
|
|
|
559
594
|
- **「已验证」现在会说明是怎么验的。** 每条 overlay 映射带一个 `evidence_tier`:`billed-model` 是供应商自己说出计费的模型(claude、grok),`client-echo` 是 CLI 记下自己送出的模型(codex、agy),`selector-only` 是 CLI 收下选择器、其余不表态。`client-echo` 是 CLI 自己抄的订单,`billed-model` 是供应商开的收据。
|
package/README.zh-TW.md
CHANGED
|
@@ -65,8 +65,9 @@ omnilane route hardest-coding "修掉會間歇失敗的 auth token 更新測試"
|
|
|
65
65
|
> **那個 export 是做什麼的?** omnilane 會用呼叫者自己的能力分數來把關每一次派工,
|
|
66
66
|
> 所以派工必須表明「是誰在問」。人類在終端機前只要設一次
|
|
67
67
|
> `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1`,或每次帶 `--operator-asserted-human`。
|
|
68
|
-
> 模型驅動 omnilane
|
|
69
|
-
>
|
|
68
|
+
> 模型驅動 omnilane 時**不能替自己主張**這個旗標。它的身分會從啟動它的 CLI 旗標
|
|
69
|
+
> (模型與強度)自動讀取,一般 session 什麼都不用帶;`omnilane whoami` 會把這個身分
|
|
70
|
+
> 印成 `--caller-context FILE`。既沒主張、又讀不到身分的話,派工會在建立工作前就被
|
|
70
71
|
> `missing-caller-context` 拒絕。
|
|
71
72
|
|
|
72
73
|
> 第一次用?先跑 `omnilane doctor`——它會告訴你 omnilane 現在能接到哪些模型 CLI 與
|
|
@@ -533,10 +534,11 @@ scripts/dispatch.sh --dry-run hardest-coding "…" # 完整解析後的計畫,
|
|
|
533
534
|
三個代碼,三種不同的修法。先跑 `omnilane doctor`——它的 `transport-overlay`
|
|
534
535
|
檢查會直接告訴你問題出在本機設定還是你的請求。
|
|
535
536
|
|
|
536
|
-
`missing-caller-context
|
|
537
|
-
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 或 `--operator-asserted-human
|
|
538
|
-
|
|
539
|
-
|
|
537
|
+
`missing-caller-context`——沒有身分送到閘門。真人加上
|
|
538
|
+
`OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` 或 `--operator-asserted-human`。
|
|
539
|
+
模型通常什麼都不用做:dispatch 會從啟動它的 CLI 讀取身分。讀不到時跑
|
|
540
|
+
`omnilane whoami`,它會印出可以帶的 `--caller-context FILE`,或說明讀不到的確切原因
|
|
541
|
+
(缺 `--effort`、模型別名、查無評分列)。模型不得替自己主張真人豁免。
|
|
540
542
|
|
|
541
543
|
`runtime-mapping-unverified`——你的身分沒問題,但**目標**沒有已驗證的本機請求
|
|
542
544
|
選擇器。可能從未探測過,也可能探測失敗;`omnilane doctor` 會回報這類設定的數量,
|
|
@@ -595,6 +597,39 @@ codex 記在 session rollout,agy 寫進 `cli.log`。這是 CLI 自己抄的訂
|
|
|
595
597
|
|
|
596
598
|
## 📜 版本歷程
|
|
597
599
|
|
|
600
|
+
## v0.42.8 新功能
|
|
601
|
+
|
|
602
|
+
- **Codex 本輪身分。** `app-server` 一律忽略啟動參數的模型與強度預設值。
|
|
603
|
+
其他 Codex 啟動保留明確的 `-m` / `--model` / `-c model=...` / `--config`;
|
|
604
|
+
沒有指定模型(包括只指定 profile)時,改讀本輪記錄。TOML 模型覆寫需要 Python 3.11+。
|
|
605
|
+
- **證據不明就拒絕。** 本程序的現行環境與 Codex 直屬子程序的初始環境,必須有相同且符合 UUID 形狀的
|
|
606
|
+
`CODEX_THREAD_ID`。記錄檔在 `$CODEX_HOME/sessions`(預設 `~/.codex`)底下找
|
|
607
|
+
`rollout-*-<對話串>.jsonl`,對話串續用之後還會有 `rollout-*-<對話串>_<工作階段>.jsonl`;
|
|
608
|
+
取最後寫入的那一份,`session_meta.id` 必須一致。最後一筆 `turn_context` 必須有模型、強度與回合 ID;
|
|
609
|
+
後面若出現**同一個回合 ID** 的 `task_complete` / `turn_complete`(讀取別名)/ `turn_aborted`,
|
|
610
|
+
就拒絕沿用過期身分,並在訊息裡寫出兩個回合 ID,以及那份記錄多久沒有被寫入。
|
|
611
|
+
- **Codex 沙箱拒絕。** 仍會先查詢祖先程序;若在 `CODEX_SANDBOX=seatbelt` 下查詢失敗,
|
|
612
|
+
`whoami` 會說明程序查詢、寫入 `~/.omnilane` 與連網都必須把指令移到沙箱外重新執行。
|
|
613
|
+
- **隱私與相容。** 逐行讀取 JSONL,不把訊息內容放進診斷;`whoami` 會回報對話串與回合 ID。
|
|
614
|
+
不從設定預設值、模型清單、封存或其他對話推斷。其他廠商、明確或繼承的身分、真人聲明
|
|
615
|
+
的優先順序不變;`OMNILANE_AA_CALLER_FROM_PROCESS=0` 同時關閉兩條讀取路徑。
|
|
616
|
+
- **測試自己清環境。** 測試不再沿用派工工人帶進來的 `OMNILANE_AA_*`;在工人裡跑時,
|
|
617
|
+
授權者身分與傳輸對照表的雜湊釘選會蓋掉各夾具自己的真人豁免,害 6 個測試失敗。
|
|
618
|
+
- **實機驗收。** 2026-09-12 在 Codex 桌面版實測:新開的對話串與續用的對話串(原始記錄檔
|
|
619
|
+
前一天就停筆)各跑一次,`whoami` 與 `dispatch.sh --dry-run` 都退出 0,讀出
|
|
620
|
+
`codex/gpt-6-astra-xhigh (score 54)`、`"allowed":true`,而且沒有建立任何工作。
|
|
621
|
+
升級:`npm i -g omnilane@0.42.8`。
|
|
622
|
+
|
|
623
|
+
## v0.42.7 新功能
|
|
624
|
+
|
|
625
|
+
- **模型 session 派工不再需要身分檔。** 沒給 `--caller-context` 時,dispatch 會沿行程樹往上找到最近的廠商 CLI,讀它啟動時帶的模型與強度。以前不在 omnilane 目錄下的 session 會卡在 `missing-caller-context`,把問題丟回給操作者——2026-09-08 與 2026-09-10 就有三個 session 這樣停下來。
|
|
626
|
+
- **`omnilane whoami`** 會把這個身分印成 caller-context 檔,讀不到時說明確切原因(缺 `--effort`、模型別名、claude 那個強度只剩 non-reasoning 列),絕不猜。
|
|
627
|
+
- **比手寫的檔案更難作假。** 閘門只檢查身分檔的格式,不核對它和實際在跑的模型是否一致。啟動旗標是 harness 設的,模型改不了,而且每個 session 各算各的:同一個模型分別開 `high` 和 `max`,上限就是 52 和 54。
|
|
628
|
+
- **明確指定仍然優先。** `--caller-context` 檔、worker 繼承的環境、`--operator-asserted-human` 都優先於自動讀取。`OMNILANE_AA_CALLER_FROM_PROCESS=0` 可以恢復「只認檔案」的規則。
|
|
629
|
+
- **被拒時會告訴你出路。** `missing-caller-context` 和重試被拒的訊息都改成指向 `omnilane whoami`,不再只給模型兩條走不通的路。
|
|
630
|
+
- **`omnilane --version` 恢復正確。** 0.42.6 發版時漏改了 `VERSION`,會回報 0.42.5。
|
|
631
|
+
- **升級。** npm 發布後執行 `npm i -g omnilane@0.42.7`。既有的 repo symlink 安裝更新檢出後確認 `omnilane --version` 即可,不需重跑安裝。
|
|
632
|
+
|
|
598
633
|
## v0.42.6 新功能
|
|
599
634
|
|
|
600
635
|
- **「已驗證」現在會說明是怎麼驗的。** 每條 overlay 映射帶一個 `evidence_tier`:`billed-model` 是供應商自己說出計費的模型(claude、grok),`client-echo` 是 CLI 記下自己送出的模型(codex、agy),`selector-only` 是 CLI 收下選擇器、其餘不表態。白話說:`client-echo` 是 CLI 自己抄的訂單,`billed-model` 是供應商開的收據。
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.42.
|
|
1
|
+
0.42.8
|
package/bin/omnilane
CHANGED
|
@@ -53,6 +53,13 @@ case "$cmd" in
|
|
|
53
53
|
;;
|
|
54
54
|
release-audit) exec bash "$REPO/scripts/release-audit.sh" "$@" ;;
|
|
55
55
|
doctor) exec bash "$REPO/scripts/doctor.sh" "$@" ;;
|
|
56
|
+
whoami)
|
|
57
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
58
|
+
echo "omnilane: whoami requires Python 3.9 or newer" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
}
|
|
61
|
+
exec python3 "$REPO/scripts/lib/caller_identity.py" "$@"
|
|
62
|
+
;;
|
|
56
63
|
benchmark)
|
|
57
64
|
command -v python3 >/dev/null 2>&1 || {
|
|
58
65
|
echo "omnilane: benchmark requires Python 3.9 or newer" >&2
|
|
@@ -89,6 +96,7 @@ omnilane — one routing table, every harness
|
|
|
89
96
|
omnilane completion bash|zsh|fish print a shell completion script
|
|
90
97
|
omnilane release-audit [flags] offline, read-only release gate
|
|
91
98
|
omnilane doctor [--json] [--strict] [--probe V] local health / opt-in live probe
|
|
99
|
+
omnilane whoami caller-context file for the CLI this runs under
|
|
92
100
|
omnilane benchmark [--json] [--run] [--vendor V] fixed quality/cost comparison
|
|
93
101
|
omnilane ui start|status|url|stop
|
|
94
102
|
omnilane configure interactive lane menu
|
package/completions/_omnilane
CHANGED
|
@@ -53,7 +53,7 @@ _omnilane() {
|
|
|
53
53
|
local command="${words[2]:-}" sub sub_index=3
|
|
54
54
|
local -a lanes ids
|
|
55
55
|
if (( CURRENT == 2 )); then
|
|
56
|
-
_values 'command' version list route dispatch jobs doctor benchmark release-audit ui configure completion help
|
|
56
|
+
_values 'command' version list route dispatch jobs doctor whoami benchmark release-audit ui configure completion help
|
|
57
57
|
return
|
|
58
58
|
fi
|
|
59
59
|
case "$command" in
|
|
@@ -58,7 +58,7 @@ _omnilane() {
|
|
|
58
58
|
prev="${COMP_WORDS[COMP_CWORD-1]:-}"
|
|
59
59
|
command="${COMP_WORDS[1]:-}"
|
|
60
60
|
if [[ "$COMP_CWORD" -eq 1 ]]; then
|
|
61
|
-
words="version list route dispatch jobs doctor benchmark release-audit ui configure completion help"
|
|
61
|
+
words="version list route dispatch jobs doctor whoami benchmark release-audit ui configure completion help"
|
|
62
62
|
else
|
|
63
63
|
case "$command" in
|
|
64
64
|
route|dispatch)
|
|
@@ -10,6 +10,7 @@ complete -c omnilane -f -n __fish_use_subcommand -a dispatch -d 'dispatch o
|
|
|
10
10
|
complete -c omnilane -f -n __fish_use_subcommand -a jobs -d 'inspect background jobs'
|
|
11
11
|
complete -c omnilane -f -n __fish_use_subcommand -a mcp -d 'MCP stdio server'
|
|
12
12
|
complete -c omnilane -f -n __fish_use_subcommand -a doctor -d 'read-only health report'
|
|
13
|
+
complete -c omnilane -f -n __fish_use_subcommand -a whoami -d 'caller-context file for the launching CLI'
|
|
13
14
|
complete -c omnilane -f -n __fish_use_subcommand -a benchmark -d 'fixed quality/cost comparison'
|
|
14
15
|
complete -c omnilane -f -n __fish_use_subcommand -a release-audit -d 'offline release gate'
|
|
15
16
|
complete -c omnilane -f -n __fish_use_subcommand -a ui -d 'Live Board server'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnilane",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.8",
|
|
4
4
|
"description": "One routing table, every harness — classify subtasks into lanes and delegate each lane through a compatible caller-owned native agent or vendor CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"omnilane": "bin/omnilane"
|
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "omnilane",
|
|
4
|
-
"version": "0.42.
|
|
4
|
+
"version": "0.42.8",
|
|
5
5
|
"description": "One routing table, every harness: classify subtasks into lanes and delegate through compatible caller-owned native agents or vendor CLIs with exact-AA downward policy and supervised jobs."
|
|
6
6
|
}
|
package/scripts/dispatch.sh
CHANGED
|
@@ -796,6 +796,17 @@ if [[ -n "${OMNILANE_AA_TRANSPORT_OVERLAY:-}" ]]; then
|
|
|
796
796
|
OMNILANE_AA_OVERLAY_SHA256="$(file_sha256 "$OMNILANE_AA_TRANSPORT_OVERLAY")"
|
|
797
797
|
export OMNILANE_AA_OVERLAY_SHA256
|
|
798
798
|
fi
|
|
799
|
+
# A model caller that passed no identity is read from the CLI that launched it:
|
|
800
|
+
# the harness sets those flags and the model cannot. An explicit file or the
|
|
801
|
+
# human assertion still wins; OMNILANE_AA_CALLER_FROM_PROCESS=0 keeps the
|
|
802
|
+
# file-only contract.
|
|
803
|
+
if [[ "$AA_OPERATOR_ASSERTED_HUMAN" == "0" && -z "$AA_CALLER_CONTEXT" \
|
|
804
|
+
&& "${OMNILANE_AA_CALLER_FROM_PROCESS:-1}" != "0" ]]; then
|
|
805
|
+
if derived_caller="$(python3 "$OMNILANE_REPO/scripts/lib/caller_identity.py" \
|
|
806
|
+
--registry "$AA_POLICY_FILE")"; then
|
|
807
|
+
AA_CALLER_CONTEXT="$derived_caller"
|
|
808
|
+
fi
|
|
809
|
+
fi
|
|
799
810
|
AA_POLICY_ACTIVE=1
|
|
800
811
|
if [[ -n "$OVERRIDE_VENDOR" || -n "$OVERRIDE_MODEL" || -n "$OVERRIDE_EFFORT" || -n "$AA_TARGET_CONFIG" ]]; then
|
|
801
812
|
AA_EXPLICIT_TARGET=1
|
package/scripts/lib/aa_policy.py
CHANGED
|
@@ -341,7 +341,10 @@ def decide(registry: dict[str, Any], registry_sha256: str, *,
|
|
|
341
341
|
if caller is None:
|
|
342
342
|
base.update(
|
|
343
343
|
code="missing-caller-context",
|
|
344
|
-
message="
|
|
344
|
+
message=("no caller identity: a model caller runs `omnilane whoami` and passes the "
|
|
345
|
+
"file it prints as --caller-context, which dispatch does itself when the "
|
|
346
|
+
"launching CLI names its model and effort; a human operator passes "
|
|
347
|
+
"--operator-asserted-human"),
|
|
345
348
|
)
|
|
346
349
|
return base
|
|
347
350
|
base["caller_context_sha256"] = caller_sha256
|
package/scripts/lib/aa_retry.py
CHANGED
|
@@ -27,7 +27,9 @@ def retry_args(directory):
|
|
|
27
27
|
original_limit = min(rows[0]["score"], original["inherited_ceiling"])
|
|
28
28
|
current_path = os.environ.get("OMNILANE_AA_CALLER_CONTEXT")
|
|
29
29
|
current_human = os.environ.get("OMNILANE_AA_OPERATOR_ASSERTED_HUMAN") == "1"
|
|
30
|
-
aa_policy._check(bool(current_path) != current_human,
|
|
30
|
+
aa_policy._check(bool(current_path) != current_human,
|
|
31
|
+
"retry requires one current caller context or explicit current human "
|
|
32
|
+
"assertion; a model caller gets one from `omnilane whoami`")
|
|
31
33
|
if current_path:
|
|
32
34
|
current, _ = aa_policy.load_caller(current_path, registry)
|
|
33
35
|
rows = aa_policy._matching_rows(registry, current["caller"])
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read the exact AA caller identity of the CLI this process runs under.
|
|
3
|
+
|
|
4
|
+
Walks up the process tree to the nearest vendor CLI and maps its selector onto
|
|
5
|
+
one scored configuration. Codex app-server always uses the host-written current
|
|
6
|
+
turn; other Codex launches do so only without an explicit model. The rollout is
|
|
7
|
+
bound to the codex direct child's initial environment, never another session.
|
|
8
|
+
|
|
9
|
+
Prints the path of a caller-context file for that identity. Exits 3 with the
|
|
10
|
+
reason on stderr when it cannot decide. It never guesses: missing selector
|
|
11
|
+
evidence, an alias, or an identity not scored exactly once is a
|
|
12
|
+
refusal, not a fallback.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import ctypes
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import struct
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Callable, Mapping, Optional
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
29
|
+
import aa_policy # noqa: E402
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import tomllib
|
|
33
|
+
except ImportError: # Explicit-model callers still work on Python 3.9/3.10.
|
|
34
|
+
tomllib = None
|
|
35
|
+
|
|
36
|
+
REPO = Path(__file__).resolve().parents[2]
|
|
37
|
+
MAX_DEPTH = 64
|
|
38
|
+
ENCODED_EFFORTS = ("xhigh", "high", "medium", "low")
|
|
39
|
+
Selector = tuple[str, Optional[str], Optional[str]]
|
|
40
|
+
Lookup = Callable[[int], Optional[tuple[int, list[str]]]]
|
|
41
|
+
EnvironmentLookup = Callable[[int], dict[str, str]]
|
|
42
|
+
UUID_PATTERN = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z")
|
|
43
|
+
TURN_END_EVENTS = frozenset(("task_complete", "turn_complete", "turn_aborted"))
|
|
44
|
+
CODEX_SANDBOX_REFUSAL = (
|
|
45
|
+
"Codex sandbox (CODEX_SANDBOX=seatbelt) blocked process inspection; omnilane also "
|
|
46
|
+
"needs to write ~/.omnilane and access the network, which this sandbox does not allow; "
|
|
47
|
+
"rerun this command outside the sandbox (approve running outside the sandbox when Codex "
|
|
48
|
+
"asks, or use a conversation with full access)"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _ProcessQueryUnavailable(Exception):
|
|
53
|
+
"""Keep a swallowed platform process-query failure available to read_caller()."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _vendor(executable: str) -> str | None:
|
|
57
|
+
path = Path(executable)
|
|
58
|
+
name = path.name
|
|
59
|
+
if name == "claude" or (path.parent.name == "versions" and path.parent.parent.name == "claude"):
|
|
60
|
+
return "claude"
|
|
61
|
+
if name == "codex":
|
|
62
|
+
return "codex"
|
|
63
|
+
if name == "grok" or (name.startswith("grok-") and path.parent.name == "downloads"):
|
|
64
|
+
return "grok"
|
|
65
|
+
if name == "agy":
|
|
66
|
+
return "gemini"
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _flag(argv: list[str], *names: str) -> str | None:
|
|
71
|
+
"""The last value given for any of names, as `--name value` or `--name=value`."""
|
|
72
|
+
value = None
|
|
73
|
+
for index, token in enumerate(argv):
|
|
74
|
+
for name in names:
|
|
75
|
+
if token == name and index + 1 < len(argv):
|
|
76
|
+
value = argv[index + 1]
|
|
77
|
+
elif token.startswith(name + "="):
|
|
78
|
+
value = token.split("=", 1)[1]
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _codex_config(argv: list[str], name: str) -> str | None:
|
|
83
|
+
selected = None
|
|
84
|
+
tokens = iter(argv)
|
|
85
|
+
for token in tokens:
|
|
86
|
+
if token == "--":
|
|
87
|
+
break
|
|
88
|
+
if token in ("-c", "--config"):
|
|
89
|
+
override = next(tokens, "")
|
|
90
|
+
elif token.startswith("--config="):
|
|
91
|
+
override = token.partition("=")[2]
|
|
92
|
+
elif token.startswith("-c"):
|
|
93
|
+
override = token[2:].removeprefix("=")
|
|
94
|
+
else:
|
|
95
|
+
continue
|
|
96
|
+
key, sep, value = override.partition("=")
|
|
97
|
+
if sep and key.strip() == name:
|
|
98
|
+
selected = value.strip()
|
|
99
|
+
if selected is None:
|
|
100
|
+
return None
|
|
101
|
+
if tomllib is None:
|
|
102
|
+
if name == "model_reasoning_effort":
|
|
103
|
+
return selected.strip("'\"")
|
|
104
|
+
raise ValueError("model config decoding requires Python 3.11+ (tomllib)")
|
|
105
|
+
try:
|
|
106
|
+
value = tomllib.loads("value = " + selected)["value"]
|
|
107
|
+
except ValueError:
|
|
108
|
+
value = selected # Codex treats invalid TOML as a literal string.
|
|
109
|
+
if not isinstance(value, str) or not value.strip():
|
|
110
|
+
raise ValueError(f"{name} config must be a non-empty string")
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _codex_subcommand(argv: list[str]) -> str | None:
|
|
115
|
+
takes_value = {"-c", "--config", "-m", "--model", "-p", "--profile", "-C", "--cd",
|
|
116
|
+
"-s", "--sandbox", "-a", "--ask-for-approval", "-i", "--image",
|
|
117
|
+
"--enable", "--disable", "--add-dir", "--local-provider"}
|
|
118
|
+
tokens = iter(argv[1:])
|
|
119
|
+
for token in tokens:
|
|
120
|
+
if token == "--":
|
|
121
|
+
return None
|
|
122
|
+
if token in takes_value:
|
|
123
|
+
next(tokens, None)
|
|
124
|
+
elif not token.startswith("-"):
|
|
125
|
+
return token
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def read_selector(argv: list[str]) -> Selector | None:
|
|
129
|
+
"""(vendor, model, effort) when argv launches a vendor CLI, otherwise None."""
|
|
130
|
+
if not argv:
|
|
131
|
+
return None
|
|
132
|
+
vendor = _vendor(argv[0])
|
|
133
|
+
rest = argv[1:]
|
|
134
|
+
if vendor == "claude":
|
|
135
|
+
return vendor, _flag(rest, "--model"), _flag(rest, "--effort")
|
|
136
|
+
if vendor == "codex":
|
|
137
|
+
if _codex_subcommand(argv) == "app-server":
|
|
138
|
+
return vendor, None, None
|
|
139
|
+
model = _flag(rest, "-m", "--model")
|
|
140
|
+
if model is None:
|
|
141
|
+
model = _codex_config(rest, "model")
|
|
142
|
+
if model is None:
|
|
143
|
+
return vendor, None, None
|
|
144
|
+
return vendor, model, _codex_config(rest, "model_reasoning_effort")
|
|
145
|
+
if vendor == "grok":
|
|
146
|
+
return vendor, _flag(rest, "-m", "--model"), _flag(rest, "--reasoning-effort")
|
|
147
|
+
if vendor == "gemini":
|
|
148
|
+
return vendor, _flag(rest, "--model", "-m"), None
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def resolve(registry: dict, vendor: str, model: str | None,
|
|
153
|
+
effort: str | None) -> tuple[dict | None, str]:
|
|
154
|
+
"""The one scored row a launch selector lands on, or None and the reason."""
|
|
155
|
+
if not model:
|
|
156
|
+
return None, f"{vendor} was launched without a model flag"
|
|
157
|
+
if vendor == "gemini" and effort is None:
|
|
158
|
+
base, _, suffix = model.rpartition("-")
|
|
159
|
+
if suffix in ENCODED_EFFORTS:
|
|
160
|
+
model, effort = base, suffix
|
|
161
|
+
if vendor == "codex":
|
|
162
|
+
if effort is None:
|
|
163
|
+
return None, (f"codex was launched without model_reasoning_effort; its configured "
|
|
164
|
+
f"default is not read, so the effort of {model} is unknown")
|
|
165
|
+
if effort == "none":
|
|
166
|
+
effort = None
|
|
167
|
+
rows = [row for row in registry["scored_configs"]
|
|
168
|
+
if row["vendor"] == vendor and row["model"] == model and row["effort"] == effort]
|
|
169
|
+
excluded = []
|
|
170
|
+
if vendor == "claude":
|
|
171
|
+
# ADR-0046: --effort has no reasoning-off value, so a non-reasoning row
|
|
172
|
+
# can never be what a Claude launch selected.
|
|
173
|
+
excluded = [row for row in rows if row["reasoning"] == "non-reasoning"]
|
|
174
|
+
rows = [row for row in rows if row["reasoning"] != "non-reasoning"]
|
|
175
|
+
if len(rows) == 1:
|
|
176
|
+
return rows[0], ""
|
|
177
|
+
if rows:
|
|
178
|
+
return None, f"{vendor} {model} at effort {effort} matches {len(rows)} scored configurations"
|
|
179
|
+
if excluded:
|
|
180
|
+
return None, (f"the only scored {model} row at effort {effort} is non-reasoning, which a "
|
|
181
|
+
f"Claude launch cannot select")
|
|
182
|
+
if vendor == "claude" and effort is None:
|
|
183
|
+
return None, f"{model} was launched without --effort and no default is scored for it"
|
|
184
|
+
return None, f"no scored configuration for {vendor} {model} at effort {effort}"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _thread_environment(entries: list[bytes]) -> dict[str, str]:
|
|
188
|
+
values = [entry.partition(b"=")[2] for entry in entries
|
|
189
|
+
if entry.partition(b"=")[0] == b"CODEX_THREAD_ID"]
|
|
190
|
+
if len(values) > 1:
|
|
191
|
+
raise ValueError("duplicate CODEX_THREAD_ID in initial environment")
|
|
192
|
+
return {"CODEX_THREAD_ID": values[0].decode(errors="replace")} if values else {}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _parse_procargs2(data: bytes) -> tuple[list[str], dict[str, str]]:
|
|
196
|
+
if len(data) < 4:
|
|
197
|
+
raise ValueError("truncated KERN_PROCARGS2 header")
|
|
198
|
+
argc = struct.unpack_from("=i", data)[0]
|
|
199
|
+
offset = data.find(b"\0", 4)
|
|
200
|
+
if argc <= 0 or offset < 0:
|
|
201
|
+
raise ValueError("invalid KERN_PROCARGS2 header")
|
|
202
|
+
while offset < len(data) and data[offset] == 0:
|
|
203
|
+
offset += 1
|
|
204
|
+
argv = []
|
|
205
|
+
for _ in range(argc):
|
|
206
|
+
end = data.find(b"\0", offset)
|
|
207
|
+
if end < 0:
|
|
208
|
+
raise ValueError("truncated KERN_PROCARGS2 argv")
|
|
209
|
+
argv.append(data[offset:end].decode(errors="replace"))
|
|
210
|
+
offset = end + 1
|
|
211
|
+
# argc, not a KEY=value search, separates argv from the initial environment.
|
|
212
|
+
entries = []
|
|
213
|
+
while offset < len(data) and data[offset] != 0:
|
|
214
|
+
end = data.find(b"\0", offset)
|
|
215
|
+
if end < 0:
|
|
216
|
+
raise ValueError("truncated KERN_PROCARGS2 environment")
|
|
217
|
+
entries.append(data[offset:end])
|
|
218
|
+
offset = end + 1
|
|
219
|
+
return argv, _thread_environment(entries)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _darwin_procargs(pid: int) -> tuple[list[str], dict[str, str]]:
|
|
223
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
224
|
+
argmax = ctypes.c_int()
|
|
225
|
+
size = ctypes.c_size_t(ctypes.sizeof(argmax))
|
|
226
|
+
if libc.sysctlbyname(b"kern.argmax", ctypes.byref(argmax), ctypes.byref(size), None, 0):
|
|
227
|
+
raise OSError(ctypes.get_errno(), "cannot read kern.argmax")
|
|
228
|
+
buffer = ctypes.create_string_buffer(argmax.value)
|
|
229
|
+
size = ctypes.c_size_t(len(buffer))
|
|
230
|
+
mib = (ctypes.c_int * 3)(1, 49, pid) # CTL_KERN, KERN_PROCARGS2.
|
|
231
|
+
if libc.sysctl(mib, 3, buffer, ctypes.byref(size), None, 0):
|
|
232
|
+
raise OSError(ctypes.get_errno(), "cannot read KERN_PROCARGS2")
|
|
233
|
+
return _parse_procargs2(buffer.raw[:size.value])
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _initial_environment(pid: int) -> dict[str, str]:
|
|
237
|
+
if sys.platform == "darwin":
|
|
238
|
+
return _darwin_procargs(pid)[1]
|
|
239
|
+
return _thread_environment(Path(f"/proc/{pid}/environ").read_bytes().split(b"\0"))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _process(pid: int) -> tuple[int, list[str]] | None:
|
|
243
|
+
"""(ppid, argv) for pid. /proc keeps argv exact; ps joins it with spaces, so
|
|
244
|
+
argv[0] comes from `comm`, which keeps a path like `Application Support` whole."""
|
|
245
|
+
proc = Path(f"/proc/{pid}")
|
|
246
|
+
if proc.is_dir():
|
|
247
|
+
try:
|
|
248
|
+
argv = [part.decode(errors="replace")
|
|
249
|
+
for part in (proc / "cmdline").read_bytes().split(b"\0") if part]
|
|
250
|
+
ppid = int((proc / "stat").read_text().rsplit(")", 1)[1].split()[1])
|
|
251
|
+
except (OSError, ValueError, IndexError):
|
|
252
|
+
return None
|
|
253
|
+
return ppid, argv
|
|
254
|
+
try:
|
|
255
|
+
head_result = subprocess.run(
|
|
256
|
+
["ps", "-o", "ppid=", "-o", "comm=", "-p", str(pid)],
|
|
257
|
+
capture_output=True, text=True, timeout=5)
|
|
258
|
+
args_result = subprocess.run(
|
|
259
|
+
["ps", "-o", "args=", "-p", str(pid)],
|
|
260
|
+
capture_output=True, text=True, timeout=5)
|
|
261
|
+
except (OSError, subprocess.SubprocessError):
|
|
262
|
+
raise _ProcessQueryUnavailable from None
|
|
263
|
+
head = head_result.stdout.strip()
|
|
264
|
+
args = args_result.stdout.strip()
|
|
265
|
+
parts = head.split(None, 1)
|
|
266
|
+
if len(parts) != 2 or not parts[0].isdigit():
|
|
267
|
+
return None
|
|
268
|
+
comm = parts[1].strip()
|
|
269
|
+
if sys.platform == "darwin" and _vendor(comm) == "codex":
|
|
270
|
+
argv, _ = _darwin_procargs(pid)
|
|
271
|
+
return int(parts[0]), argv
|
|
272
|
+
rest = args[len(comm):] if args.startswith(comm) else args.partition(" ")[2]
|
|
273
|
+
return int(parts[0]), [comm, *rest.split()]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _launcher(pid: int, lookup: Lookup) -> tuple[int, Selector, int | None] | None:
|
|
277
|
+
"""The nearest process at or above pid that is a vendor CLI, with its selector.
|
|
278
|
+
|
|
279
|
+
Nearest wins: a codex worker started by a Claude session is a codex caller.
|
|
280
|
+
"""
|
|
281
|
+
seen: set[int] = set()
|
|
282
|
+
child = None
|
|
283
|
+
for _ in range(MAX_DEPTH):
|
|
284
|
+
if pid <= 0 or pid in seen:
|
|
285
|
+
return None
|
|
286
|
+
seen.add(pid)
|
|
287
|
+
entry = lookup(pid)
|
|
288
|
+
if entry is None:
|
|
289
|
+
return None
|
|
290
|
+
ppid, argv = entry
|
|
291
|
+
selector = read_selector(argv)
|
|
292
|
+
if selector is not None:
|
|
293
|
+
return pid, selector, child
|
|
294
|
+
child = pid
|
|
295
|
+
pid = ppid
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def find_launcher(pid: int, lookup: Lookup = _process) -> tuple[int, Selector] | None:
|
|
300
|
+
try:
|
|
301
|
+
found = _launcher(pid, lookup)
|
|
302
|
+
except _ProcessQueryUnavailable:
|
|
303
|
+
return None
|
|
304
|
+
return found[:2] if found else None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _rollout_age_hint(timestamp: object, now: Callable[[], datetime] | None) -> str:
|
|
308
|
+
"""Describe the last record without letting unusable time data change a refusal."""
|
|
309
|
+
if not isinstance(timestamp, str):
|
|
310
|
+
return ""
|
|
311
|
+
try:
|
|
312
|
+
recorded = datetime.fromisoformat(timestamp[:-1] + "+00:00" if timestamp.endswith("Z") else timestamp)
|
|
313
|
+
current = now() if now is not None else datetime.now(timezone.utc)
|
|
314
|
+
if recorded.tzinfo is None or current.tzinfo is None:
|
|
315
|
+
return ""
|
|
316
|
+
seconds = (current - recorded).total_seconds()
|
|
317
|
+
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
318
|
+
return ""
|
|
319
|
+
if seconds < 0:
|
|
320
|
+
return ""
|
|
321
|
+
# Floor display units; compare the unrounded duration against the hint threshold.
|
|
322
|
+
if seconds < 3600:
|
|
323
|
+
age = f"{int(seconds // 60)}m"
|
|
324
|
+
elif seconds < 86400:
|
|
325
|
+
age = f"{int(seconds // 3600)}h"
|
|
326
|
+
else:
|
|
327
|
+
age = f"{int(seconds // 86400)}d"
|
|
328
|
+
hint = f"; that rollout's last record is {timestamp}, {age} before now"
|
|
329
|
+
if seconds > 600:
|
|
330
|
+
hint += ", so CODEX_THREAD_ID may name an earlier conversation than the one running this command"
|
|
331
|
+
return hint
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _rollout_candidates(home: Path, thread: str) -> list[Path]:
|
|
335
|
+
"""Resuming a thread opens a second rollout named <thread>_<session>, so match both forms."""
|
|
336
|
+
sessions = home / "sessions"
|
|
337
|
+
found = set(sessions.glob(f"*/*/*/rollout-*-{thread}.jsonl"))
|
|
338
|
+
found |= set(sessions.glob(f"*/*/*/rollout-*-{thread}_*.jsonl"))
|
|
339
|
+
return sorted(found, key=lambda path: (path.stat().st_mtime, path.name))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _rollout_selector(thread: str,
|
|
343
|
+
current_environment: Mapping[str, str] = os.environ,
|
|
344
|
+
now: Callable[[], datetime] | None = None) -> tuple[Selector, str]:
|
|
345
|
+
home = Path(current_environment.get("CODEX_HOME") or Path.home() / ".codex")
|
|
346
|
+
try:
|
|
347
|
+
paths = _rollout_candidates(home, thread)
|
|
348
|
+
except OSError as error:
|
|
349
|
+
raise ValueError(f"cannot read rollout (errno {error.errno})") from None
|
|
350
|
+
if not paths:
|
|
351
|
+
raise ValueError("expected an active rollout for this thread; found 0")
|
|
352
|
+
# The most recently written file is the live one; older files belong to earlier sessions.
|
|
353
|
+
path = paths[-1]
|
|
354
|
+
metadata = 0
|
|
355
|
+
latest = None
|
|
356
|
+
ended = None
|
|
357
|
+
last_timestamp = None
|
|
358
|
+
try:
|
|
359
|
+
with path.open(encoding="utf-8") as stream:
|
|
360
|
+
for line in stream:
|
|
361
|
+
record = json.loads(line)
|
|
362
|
+
if not isinstance(record, dict):
|
|
363
|
+
raise ValueError("invalid rollout record")
|
|
364
|
+
last_timestamp = record.get("timestamp")
|
|
365
|
+
kind = record.get("type")
|
|
366
|
+
if kind not in ("session_meta", "turn_context", "event_msg"):
|
|
367
|
+
continue
|
|
368
|
+
payload = record.get("payload")
|
|
369
|
+
if not isinstance(payload, dict):
|
|
370
|
+
raise ValueError("invalid rollout payload")
|
|
371
|
+
if kind == "session_meta":
|
|
372
|
+
metadata += 1
|
|
373
|
+
if payload.get("id") != thread:
|
|
374
|
+
raise ValueError("session_meta id does not match CODEX_THREAD_ID")
|
|
375
|
+
elif kind == "turn_context":
|
|
376
|
+
latest = {key: payload.get(key) for key in ("turn_id", "model", "effort")}
|
|
377
|
+
ended = None
|
|
378
|
+
elif kind == "event_msg":
|
|
379
|
+
event = payload.get("type")
|
|
380
|
+
if not isinstance(event, str):
|
|
381
|
+
raise ValueError("invalid rollout event type")
|
|
382
|
+
if latest is not None and event in TURN_END_EVENTS:
|
|
383
|
+
event_turn = payload.get("turn_id")
|
|
384
|
+
if not isinstance(event_turn, str) or not event_turn.strip():
|
|
385
|
+
event_turn = None
|
|
386
|
+
context_turn = latest["turn_id"]
|
|
387
|
+
if not isinstance(context_turn, str) or not context_turn.strip():
|
|
388
|
+
context_turn = None
|
|
389
|
+
if event_turn is None or context_turn is None or event_turn == context_turn:
|
|
390
|
+
ended = (event, context_turn or "unknown", event_turn or "unknown")
|
|
391
|
+
except (json.JSONDecodeError, UnicodeError):
|
|
392
|
+
raise ValueError("rollout contains incomplete or invalid JSON") from None
|
|
393
|
+
except OSError as error:
|
|
394
|
+
raise ValueError(f"cannot read rollout (errno {error.errno})") from None
|
|
395
|
+
if metadata != 1:
|
|
396
|
+
raise ValueError("expected exactly one matching session_meta")
|
|
397
|
+
if latest is None:
|
|
398
|
+
raise ValueError("rollout has no turn_context")
|
|
399
|
+
if ended:
|
|
400
|
+
event, context_turn, event_turn = ended
|
|
401
|
+
raise ValueError(f"latest turn_context turn {context_turn} has already ended "
|
|
402
|
+
f"({event}, turn {event_turn})" + _rollout_age_hint(last_timestamp, now))
|
|
403
|
+
for key, value in latest.items():
|
|
404
|
+
if not isinstance(value, str) or not value.strip():
|
|
405
|
+
detail = " (turn unknown)" if key == "turn_id" else ""
|
|
406
|
+
raise ValueError(f"latest turn_context {key} must be a non-empty string{detail}")
|
|
407
|
+
return ("codex", latest["model"], latest["effort"]), f"thread {thread}, turn {latest['turn_id']}"
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def read_caller(pid: int, lookup: Lookup = _process,
|
|
411
|
+
environment: EnvironmentLookup = _initial_environment,
|
|
412
|
+
current_environment: Mapping[str, str] = os.environ,
|
|
413
|
+
now: Callable[[], datetime] | None = None) -> tuple[int, Selector, str]:
|
|
414
|
+
if current_environment.get("OMNILANE_AA_CALLER_FROM_PROCESS") == "0":
|
|
415
|
+
raise ValueError("caller identity from process is disabled")
|
|
416
|
+
try:
|
|
417
|
+
found = _launcher(pid, lookup)
|
|
418
|
+
except _ProcessQueryUnavailable:
|
|
419
|
+
if current_environment.get("CODEX_SANDBOX") == "seatbelt":
|
|
420
|
+
raise ValueError(CODEX_SANDBOX_REFUSAL) from None
|
|
421
|
+
found = None
|
|
422
|
+
except OSError as error:
|
|
423
|
+
if current_environment.get("CODEX_SANDBOX") == "seatbelt":
|
|
424
|
+
raise ValueError(CODEX_SANDBOX_REFUSAL) from None
|
|
425
|
+
raise ValueError(f"cannot query caller process (errno {error.errno})") from None
|
|
426
|
+
if found is None:
|
|
427
|
+
if current_environment.get("CODEX_SANDBOX") == "seatbelt":
|
|
428
|
+
raise ValueError(CODEX_SANDBOX_REFUSAL)
|
|
429
|
+
raise ValueError("no vendor CLI among this process's ancestors; a model caller passes "
|
|
430
|
+
"--caller-context FILE and a human operator --operator-asserted-human")
|
|
431
|
+
launcher, selector, child = found
|
|
432
|
+
if selector[0] != "codex" or selector[1] is not None:
|
|
433
|
+
return launcher, selector, ""
|
|
434
|
+
thread = current_environment.get("CODEX_THREAD_ID")
|
|
435
|
+
if not thread or not UUID_PATTERN.fullmatch(thread):
|
|
436
|
+
raise ValueError("current process CODEX_THREAD_ID is missing or not a UUID")
|
|
437
|
+
if child is None:
|
|
438
|
+
raise ValueError("cannot find the codex direct child in the ancestor chain")
|
|
439
|
+
try:
|
|
440
|
+
inherited = environment(child).get("CODEX_THREAD_ID")
|
|
441
|
+
except (OSError, ValueError) as error:
|
|
442
|
+
detail = f"errno {error.errno}" if isinstance(error, OSError) else "invalid environment block"
|
|
443
|
+
raise ValueError(f"cannot read codex direct child initial environment ({detail})") from None
|
|
444
|
+
if not inherited or not UUID_PATTERN.fullmatch(inherited):
|
|
445
|
+
raise ValueError("codex direct child CODEX_THREAD_ID is missing or not a UUID")
|
|
446
|
+
if inherited != thread:
|
|
447
|
+
raise ValueError("CODEX_THREAD_ID mismatch between current process and codex direct child")
|
|
448
|
+
selector, source = _rollout_selector(thread, current_environment, now)
|
|
449
|
+
return launcher, selector, source
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def load_registry(path: str | Path) -> tuple[dict, str]:
|
|
453
|
+
"""The approved registry without the transport overlay, which says nothing
|
|
454
|
+
about the caller and must not stop one from learning who it is."""
|
|
455
|
+
saved = {key: os.environ.pop(key)
|
|
456
|
+
for key in ("OMNILANE_AA_TRANSPORT_OVERLAY", "OMNILANE_AA_OVERLAY_SHA256")
|
|
457
|
+
if key in os.environ}
|
|
458
|
+
try:
|
|
459
|
+
return aa_policy.load_registry(path)
|
|
460
|
+
finally:
|
|
461
|
+
os.environ.update(saved)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def write_context(row: dict, registry: dict, home: Path) -> Path:
|
|
465
|
+
"""One file per identity: every session launched the same way is the same caller."""
|
|
466
|
+
directory = Path(home) / "caller-context"
|
|
467
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
468
|
+
path = directory / (row["id"].replace("/", "--") + ".json")
|
|
469
|
+
text = json.dumps({
|
|
470
|
+
"schema_version": 1,
|
|
471
|
+
"snapshot_id": registry["snapshot"]["id"],
|
|
472
|
+
"kind": "model",
|
|
473
|
+
"caller": {key: row[key] for key in aa_policy.IDENTITY_FIELDS},
|
|
474
|
+
"inherited_ceiling": row["score"],
|
|
475
|
+
}, indent=2, sort_keys=True) + "\n"
|
|
476
|
+
try:
|
|
477
|
+
if path.read_text() == text:
|
|
478
|
+
return path
|
|
479
|
+
except OSError:
|
|
480
|
+
pass
|
|
481
|
+
staging = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
482
|
+
staging.write_text(text)
|
|
483
|
+
os.replace(staging, path)
|
|
484
|
+
return path
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def main(argv: list[str] | None = None, environment: Mapping[str, str] = os.environ,
|
|
488
|
+
lookup: Lookup = _process,
|
|
489
|
+
process_environment: EnvironmentLookup = _initial_environment) -> int:
|
|
490
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
491
|
+
parser.add_argument("--registry",
|
|
492
|
+
default=os.environ.get("OMNILANE_AA_POLICY_FILE")
|
|
493
|
+
or str(REPO / "config" / "aa-model-policy.json"),
|
|
494
|
+
help="frozen AA registry (default: the repository copy)")
|
|
495
|
+
args = parser.parse_args(argv)
|
|
496
|
+
try:
|
|
497
|
+
registry, _ = load_registry(args.registry)
|
|
498
|
+
except (aa_policy.PolicyError, OSError, ValueError) as error:
|
|
499
|
+
print(f"omnilane: cannot read the AA registry: {error}", file=sys.stderr)
|
|
500
|
+
return 3
|
|
501
|
+
try:
|
|
502
|
+
pid, (vendor, model, effort), source = read_caller(
|
|
503
|
+
os.getpid(), lookup, process_environment, environment)
|
|
504
|
+
except ValueError as error:
|
|
505
|
+
print(f"omnilane: cannot read the caller identity: {error}", file=sys.stderr)
|
|
506
|
+
return 3
|
|
507
|
+
row, reason = resolve(registry, vendor, model, effort)
|
|
508
|
+
if row is None:
|
|
509
|
+
print(f"omnilane: cannot read the caller identity from pid {pid}: {reason}", file=sys.stderr)
|
|
510
|
+
return 3
|
|
511
|
+
home = Path(os.environ.get("OMNILANE_HOME") or Path.home() / ".omnilane")
|
|
512
|
+
path = write_context(row, registry, home)
|
|
513
|
+
provenance = f", {source}" if source else ""
|
|
514
|
+
print(f"omnilane: caller is {row['id']} (score {row['score']}), read from pid {pid}{provenance}",
|
|
515
|
+
file=sys.stderr)
|
|
516
|
+
print(path)
|
|
517
|
+
return 0
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
if __name__ == "__main__":
|
|
521
|
+
raise SystemExit(main())
|
|
@@ -142,7 +142,13 @@ if [[ "$RC" -eq 142 || "$RC" -eq 124 ]]; then
|
|
|
142
142
|
thread_id="$(head -1 "${OUTPUT_FILE}.progress.log" 2>/dev/null |
|
|
143
143
|
sed -n 's/.*"thread_id":"\([^"]*\)".*/\1/p')"
|
|
144
144
|
if [[ -n "$thread_id" ]]; then
|
|
145
|
-
|
|
145
|
+
# A resumed thread writes rollout-*-<thread>_<session>.jsonl, so match both names and keep the newest.
|
|
146
|
+
rollout=""
|
|
147
|
+
while IFS= read -r candidate; do
|
|
148
|
+
[[ -z "$candidate" ]] && continue
|
|
149
|
+
if [[ -z "$rollout" || "$candidate" -nt "$rollout" ]]; then rollout="$candidate"; fi
|
|
150
|
+
done < <(find "${CODEX_HOME:-$HOME/.codex}/sessions" \
|
|
151
|
+
\( -name "rollout-*-${thread_id}.jsonl" -o -name "rollout-*-${thread_id}_*.jsonl" \) 2>/dev/null)
|
|
146
152
|
echo "omnilane: rollout: ${rollout:-not found (thread ${thread_id})}"
|
|
147
153
|
else
|
|
148
154
|
echo "omnilane: rollout: unknown — no thread_id in the progress log, so codex died before its first event"
|
package/skills/omnilane/SKILL.md
CHANGED
|
@@ -19,10 +19,14 @@ You (the main loop) may be Claude, GPT, Grok, or Gemini. The procedure is identi
|
|
|
19
19
|
Read-only work uses advise; edits require `--mode work --workdir <repo>`.
|
|
20
20
|
`<repo>/scripts/dispatch.sh --caller-context FILE [--executor auto|native|cli] [--native-context FILE] [--vendor V] [--mode work] [--workdir DIR] <lane> "<task>"`
|
|
21
21
|
|
|
22
|
-
A model caller
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
A model caller needs a verifiable caller identity. Codex app-server reads the
|
|
23
|
+
current turn from its rollout; other launches read the model and effort from
|
|
24
|
+
the CLI that launched you, so an ordinary session passes nothing. When that
|
|
25
|
+
identity cannot be read, dispatch is refused with `missing-caller-context`
|
|
26
|
+
before a job exists: run `omnilane whoami` (or `<repo>/bin/omnilane whoami`)
|
|
27
|
+
and pass the file it prints as `--caller-context`. See **Frozen exact-AA
|
|
28
|
+
downward gate** for the schema and what to do when effort is genuinely
|
|
29
|
+
unverifiable.
|
|
26
30
|
|
|
27
31
|
Add `--background` for long tasks; poll with `scripts/jobs.sh status|result <id>`.
|
|
28
32
|
Use `--thread NAME` when later claude, codex, grok or gemini dispatches
|
|
@@ -355,15 +359,37 @@ and `snapshot_id` must equal the registry's own `snapshot.id`.
|
|
|
355
359
|
Set `inherited_ceiling` to your own row's score when you are the root caller, or
|
|
356
360
|
to the ceiling you were handed when you are a child.
|
|
357
361
|
|
|
358
|
-
**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
362
|
+
**Your identity is read from the CLI that launched you.** Without explicit or
|
|
363
|
+
inherited caller-context, dispatch finds the nearest vendor CLI. Claude, Grok
|
|
364
|
+
and Agy retain their launch selectors. Codex outside app-server retains explicit
|
|
365
|
+
`-m` / `--model` or `-c model=...` / `--config` selectors; TOML model overrides
|
|
366
|
+
require Python 3.11+. A profile is not an explicit model selector.
|
|
367
|
+
|
|
368
|
+
Codex app-server always ignores startup selectors, even explicit model flags;
|
|
369
|
+
other Codex launches without a model use the same current-turn rollout reader.
|
|
370
|
+
It requires matching UUID-shaped `CODEX_THREAD_ID` values in this process and the
|
|
371
|
+
codex direct child's initial environment (not text embedded in argv). Exactly
|
|
372
|
+
one active rollout under `$CODEX_HOME/sessions` (default `~/.codex`) must have
|
|
373
|
+
matching `session_meta.id`. The last `turn_context` must provide non-empty model,
|
|
374
|
+
effort and turn id, with no later `task_complete`, `turn_complete` or
|
|
375
|
+
`turn_aborted` event.
|
|
376
|
+
Missing, ambiguous, malformed or stale evidence refuses: no config defaults,
|
|
377
|
+
model-list, archive or other-thread fallback. JSONL is streamed; only identity
|
|
378
|
+
metadata and event types reach diagnostics, never message content.
|
|
379
|
+
|
|
380
|
+
`omnilane whoami` prints the resulting caller-context path and reports thread
|
|
381
|
+
and turn ids for rollout evidence. The existing scored-row resolver still makes
|
|
382
|
+
the decision; this is host request-selector evidence, not proof of upstream
|
|
383
|
+
provider identity. Nearest wins, including workers launched by another vendor.
|
|
384
|
+
Explicit `--caller-context`, inherited identity and `--operator-asserted-human`
|
|
385
|
+
retain their precedence; `OMNILANE_AA_CALLER_FROM_PROCESS=0` disables both readers.
|
|
386
|
+
|
|
387
|
+
Ancestor lookup still runs first. If it fails under `CODEX_SANDBOX=seatbelt`,
|
|
388
|
+
`whoami` explains that process inspection, `~/.omnilane` writes and networking
|
|
389
|
+
require rerunning the command outside the Codex sandbox; do not retry with a
|
|
390
|
+
caller-context inside that sandbox.
|
|
391
|
+
|
|
392
|
+
Only when `omnilane whoami` refuses: ask the operator, or declare the
|
|
367
393
|
lowest-scoring row of your model and say so in your report. Understating only
|
|
368
394
|
narrows what you may dispatch to, so it fails in the safe direction — but it is
|
|
369
395
|
the fallback, not the first move, and an unnecessarily low ceiling silently
|
|
@@ -372,7 +398,9 @@ declared effort to unblock a refused target, and never assert
|
|
|
372
398
|
`--operator-asserted-human` on your own behalf.
|
|
373
399
|
|
|
374
400
|
Three refusal codes mean different things and need different fixes.
|
|
375
|
-
`missing-caller-context` means you passed no file
|
|
401
|
+
`missing-caller-context` means no identity reached the gate: you passed no file
|
|
402
|
+
and dispatch could not read one from your launching CLI. Run `omnilane whoami`;
|
|
403
|
+
its refusal says exactly why, and that reason is what to fix or report.
|
|
376
404
|
`runtime-mapping-unverified` means the file is fine but the *target* has no proven
|
|
377
405
|
host-local request selector; that is fixed by a `--transport-overlay` entry backed
|
|
378
406
|
by real evidence, never by editing the frozen registry (its sha256 is pinned in
|