mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
fix+release(v0.4.2): D75 — codex CLI v0.133.0 schema + per-hop model override (#47)
Patch release fixing 5 bugs caught by real-machine E2E testing on PI231 +
Mac mini (2026-05-26 session). Prior D-day reviewers + the post-v0.4.0
maintainer review all missed these because they reviewed against spec text
and against the local OLP install's cached codex CLI shape, not against a
fresh `npm install -g @openai/codex` on a remote operator host getting
v0.133.0 for the first time.
F1 — codex auth.json schema pin (lib/providers/codex.mjs readAuthArtifact)
Real codex CLI v0.133.0 nests the access token under `tokens.access_token`,
not at top-level access_token / token / accessToken. Pre-D75 readAuthArtifact
returned null → OLP reported "auth artifact missing" even for fully
logged-in users. Fix: prepend creds?.tokens?.access_token to the precedence
chain at both override + default branches. Legacy fields preserved as
fallback. Authority: codex CLI v0.133.0 on-disk auth.json shape verified
empirically on PI231 2026-05-26 E2E session.
F2 — codex spawn args + --skip-git-repo-check (lib/providers/codex.mjs irToCodex)
codex CLI v0.133.0 trusted-directory sandbox refuses with "Not inside a
trusted directory" outside git repos. OLP deploys typically outside a git
repo. Fix: add '--skip-git-repo-check' to args before '--model'. Authority:
codex CLI v0.133.0 reference (`codex exec --help` documents the flag).
F3 — codex NDJSON event shape pin (lib/providers/codex.mjs codexChunkToIR)
Real v0.133.0 stream: thread.started → turn.started → item.completed
(item.type='agent_message', item.text=<response>) → turn.completed.
D6 defensive parser only recognised top-level content/delta/text +
type:'stop'/done:true → every chunk silently dropped → response body had
content: null. Fix: add three new recognisers (item.completed → delta;
turn.completed → stop; turn.failed → error) before the legacy fallback
chain. Legacy recognisers preserved for backward/forward compat.
F4 — `olp status` reads body.stats.cache.size, not body.cache.entries
(bin/olp.mjs cmdStatus). Server payload nests stats under stats.cache;
CacheStore.stats() exposes {hits, misses, size, inflightCount} — there is
no `entries` field. D74 P2-3 fixed cmdUsage + cmdCache for the same bug
class but missed cmdStatus.
F7 — per-hop chain `model` overrides IR model in provider.spawn()
(server.mjs executeHopFn + streaming sourceFactory). Pre-D75 executeHopFn
used hopModel for cache key + audit ctx but passed the original irReq
(with irReq.model = user's request) to provider.spawn(). Chain config
[{anthropic, claude-X}, {openai, gpt-5.5}] would always spawn BOTH plugins
with --model claude-X — openai rejected the unknown model and the chain
died. This broke the core OLP value prop (cross-provider fallback with
provider-appropriate model substitution). Fix: build per-hop IR variant
with { ...irReq, model: hopModel } and pass to spawn. Conditional skips
clone when hopModel === irReq.model. Applied to BOTH buffered path AND
streaming path. Authority: ADR 0004 § Chain advancement step 1 (per-hop
config supplies provider AND model — contract always specified, code
didn't complete it).
Out of scope (deferred to Phase 5):
- F5 (server bind / OLP_BIND env) — needs anonymous-key trust review
- F6 (doctor client-vs-server-side limit) — needs trigger-taxonomy ADR
Test count: 704 → 714 (+10 Suite 36 D75 regression tests: 36i–36r).
Files touched: lib/providers/codex.mjs, bin/olp.mjs, server.mjs,
test-features.mjs, package.json, CHANGELOG.md.
Phase 5 process learning: every provider plugin D-day must include a
real-CLI E2E on a remote operator host before merging — not on the
maintainer workstation (which may have an older CLI cached from a prior
install). D6/D7 codex E2E was deferred and that deferral compounded across
3 layers. F7 reinforces a separate lesson: when a function signature takes
(provider, model, ir), reviewers must check that `model` is consumed
everywhere downstream — not just at the call site they happened to look at.
Authority: ADR 0002 (provider contract — codex plugin), ADR 0004 (fallback
engine — per-hop model contract), lib/providers/codex.mjs D6 assumption
A2/A3/A4 docstrings (which all said "D7 will pin" and D7 never did); codex
CLI v0.133.0 on-disk schema + `codex exec --help` output verified
empirically on PI231 (2026-05-26 E2E session); Iron Rule 第二律
evidence-over-should-work; CLAUDE.md release_kit.phase_rolling_mode
cross-Phase discipline ("hotfix to a shipped Phase N deliverable → bump
patch, tag, release before next push").
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+108
-3
@@ -157,6 +157,36 @@ function resolveCodexBin() {
|
||||
// D6 assumption A2: auth file is named auth.json (unconfirmed — D7 will pin).
|
||||
// D6 assumption A3: access token field is `access_token` or `token` (unconfirmed).
|
||||
//
|
||||
// ── D75 (v0.4.2) F1 — codex CLI v0.133.0 schema pin ─────────────────────────
|
||||
//
|
||||
// Real codex CLI v0.133.0 auth.json (verified empirically on PI231 / Mac mini,
|
||||
// 2026-05-26 E2E session):
|
||||
//
|
||||
// {
|
||||
// "auth_mode": "chatgpt",
|
||||
// "OPENAI_API_KEY": null | "<key>",
|
||||
// "tokens": {
|
||||
// "id_token": "<JWT>",
|
||||
// "access_token": "<opaque-or-JWT>", <-- THIS is the access token
|
||||
// "refresh_token": "<opaque>",
|
||||
// "account_id": "<uuid>"
|
||||
// },
|
||||
// "last_refresh": "<ISO8601>"
|
||||
// }
|
||||
//
|
||||
// D6 assumption A3 originally tried `creds.access_token` at TOP level. Under
|
||||
// codex v0.133.0 this field does not exist at the top level → readAuthArtifact()
|
||||
// returned null even when the user had fully completed `codex login`. OLP then
|
||||
// reported "auth artifact missing" via /health and `olp doctor`, and refused
|
||||
// to spawn codex — false negative blocking the entire openai provider.
|
||||
//
|
||||
// Fix: prepend `creds?.tokens?.access_token` to the precedence chain. Keep all
|
||||
// existing fallbacks unchanged so older codex CLI versions (pre-v0.133) and any
|
||||
// future shape variants still resolve.
|
||||
//
|
||||
// Authority pin: codex CLI v0.133.0 source + on-disk auth.json captured during
|
||||
// PI231 E2E. See D75 commit body for the verification transcript.
|
||||
//
|
||||
// Returns { accessToken: string } or null (never throws).
|
||||
export function readAuthArtifact() {
|
||||
// 1. Explicit test override — always takes precedence.
|
||||
@@ -165,7 +195,12 @@ export function readAuthArtifact() {
|
||||
try {
|
||||
const raw = readFileSync(authPathOverride, 'utf8');
|
||||
const creds = JSON.parse(raw);
|
||||
const token = creds?.access_token ?? creds?.token ?? creds?.accessToken;
|
||||
// D75 F1: codex CLI v0.133.0 nests the token under `tokens.access_token`.
|
||||
// Preserve top-level fallbacks for backward / forward compat.
|
||||
const token = creds?.tokens?.access_token
|
||||
?? creds?.access_token
|
||||
?? creds?.token
|
||||
?? creds?.accessToken;
|
||||
if (token && typeof token === 'string') return { accessToken: token };
|
||||
} catch { /* fall through */ }
|
||||
return null; // explicit path set but file missing / malformed
|
||||
@@ -178,8 +213,12 @@ export function readAuthArtifact() {
|
||||
try {
|
||||
const raw = readFileSync(authPath, 'utf8');
|
||||
const creds = JSON.parse(raw);
|
||||
// D6 assumption A3: try common OAuth field names in precedence order.
|
||||
const token = creds?.access_token ?? creds?.token ?? creds?.accessToken;
|
||||
// D75 F1: codex CLI v0.133.0 nests the token under `tokens.access_token`.
|
||||
// Try the nested location FIRST, then fall back to legacy top-level fields.
|
||||
const token = creds?.tokens?.access_token
|
||||
?? creds?.access_token
|
||||
?? creds?.token
|
||||
?? creds?.accessToken;
|
||||
if (token && typeof token === 'string') return { accessToken: token };
|
||||
} catch { /* file missing or malformed */ }
|
||||
|
||||
@@ -242,9 +281,30 @@ export function irToCodex(irRequest) {
|
||||
// model string (e.g., gpt-5.5, gpt-5.4, gpt-5.3-codex).
|
||||
// PROMPT: "Initial instruction for the task. Use '-' to pipe the prompt
|
||||
// from stdin."
|
||||
//
|
||||
// ── D75 (v0.4.2) F2 — codex CLI v0.133.0 trusted-directory sandbox ─────────
|
||||
// codex CLI v0.133.0 added a trusted-directory sandbox: invocations outside
|
||||
// a git repo (or outside any directory explicitly trusted via
|
||||
// `codex config trusted-directories`) refuse with:
|
||||
// "Not inside a trusted directory and --skip-git-repo-check was not specified."
|
||||
// and exit non-zero with zero NDJSON output → OLP surfaces SPAWN_FAILED with
|
||||
// no usable chunks → fallback engine advances to next hop unnecessarily.
|
||||
//
|
||||
// The CWD that OLP spawns from is typically the server install dir (`~/olp/`
|
||||
// on Pi231) which is a git repo on maintainer workstations but is NOT a git
|
||||
// repo on most operator hosts. We bypass the sandbox unconditionally because
|
||||
// OLP is the trusted caller (it is the operator's own server invoking its own
|
||||
// configured Codex subscription via the documented `codex exec` automation
|
||||
// entry point). The trusted-directory sandbox is a foot-gun safeguard for
|
||||
// interactive users; OLP's spawn is non-interactive and pre-authorized.
|
||||
//
|
||||
// Authority: codex CLI v0.133.0 release notes / `codex exec --help` output
|
||||
// documenting `--skip-git-repo-check`. Verified empirically on PI231 E2E
|
||||
// 2026-05-26.
|
||||
const args = [
|
||||
'exec',
|
||||
'--json',
|
||||
'--skip-git-repo-check',
|
||||
'--model', irRequest.model,
|
||||
];
|
||||
|
||||
@@ -291,6 +351,51 @@ export function codexChunkToIR(rawNDJSONLine) {
|
||||
|
||||
if (!event || typeof event !== 'object') return null;
|
||||
|
||||
// ── D75 (v0.4.2) F3 — codex CLI v0.133.0 event shape pin ─────────────────
|
||||
// Real codex CLI v0.133.0 NDJSON event stream (verified empirically on PI231
|
||||
// / Mac mini, 2026-05-26 E2E session):
|
||||
// {"type":"thread.started","thread_id":"019e..."}
|
||||
// {"type":"turn.started"}
|
||||
// {"type":"item.started","item":{"id":"item_0","type":"reasoning","text":""}}
|
||||
// {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"<response>"}}
|
||||
// {"type":"turn.completed","usage":{"input_tokens":..,"output_tokens":..}}
|
||||
//
|
||||
// The D6 defensive parser recognized `content`/`delta`/`text` fields at the
|
||||
// top level and `type === 'stop'`/`done === true`. None of these match
|
||||
// v0.133.0's actual shape → every chunk was silently dropped → response body
|
||||
// had `content: null`. F3 adds three NEW recognizers (item.completed →
|
||||
// agent_message; turn.completed → stop; turn.failed → error) BEFORE the
|
||||
// legacy fallback chain. Legacy recognizers preserved for forward/backward
|
||||
// compat (older codex versions; future shape variants).
|
||||
|
||||
// F3-a: agent_message item completion.
|
||||
// codex v0.133.0 emits assistant text as a single item.completed event whose
|
||||
// item.type is 'agent_message' and item.text carries the full text. There
|
||||
// are no incremental deltas — the entire response arrives in one chunk.
|
||||
if (event.type === 'item.completed'
|
||||
&& event.item?.type === 'agent_message'
|
||||
&& typeof event.item?.text === 'string') {
|
||||
return { type: 'delta', content: event.item.text };
|
||||
}
|
||||
|
||||
// F3-b: turn completion → stop chunk.
|
||||
// codex v0.133.0 emits turn.completed with a usage block when the model
|
||||
// finishes. We map this to IR stop with finish_reason 'stop'.
|
||||
if (event.type === 'turn.completed') {
|
||||
return { type: 'stop', finish_reason: 'stop' };
|
||||
}
|
||||
|
||||
// F3-c: turn failure → error chunk.
|
||||
// codex v0.133.0 emits turn.failed with an embedded error object when the
|
||||
// turn cannot complete. Extract a human-readable message for the IR error.
|
||||
if (event.type === 'turn.failed') {
|
||||
const errMsg = (typeof event.error === 'string')
|
||||
? event.error
|
||||
: (event.error?.message ?? 'codex turn.failed');
|
||||
return { type: 'error', error: errMsg };
|
||||
}
|
||||
|
||||
// ── Legacy/fallback recognizers (kept for backward + forward compat) ────
|
||||
// Error event: type === 'error' or error field present
|
||||
// A4: defensive — error shape unconfirmed; D7 will pin actual field names
|
||||
if (event.type === 'error' || (event.error && typeof event.error === 'string')) {
|
||||
|
||||
Reference in New Issue
Block a user