Skip to content

feat: hermes/omp file-free routing (#535 phases 2+3) + MITM cert strictness fix - #538

Merged
ranxianglei merged 1 commit into
2026-09-04_535-pi-file-freefrom
2026-09-04_535-hermes-omp-file-free
Sep 5, 2026
Merged

ranxianglei merged 1 commit into
2026-09-04_535-pi-file-freefrom
2026-09-04_535-hermes-omp-file-free

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Stacked on #536 (phase 1, pi file-free) — merge #536 first; GitHub retargets this PR to master when its base is deleted after merge. Diff below includes only the incremental changes on top of #536.

#535 phases 2+3: hermes + omp join pi in file-free injection (no shadow homes, no overlay dirs, real ~/.hermes / ~/.omp untouched).

hermes (phase 2) — env-only routing

  • Child env: stripInheritedProxy + HTTPS_PROXY=<proxy> + HERMES_CA_BUNDLE=<combined bundle>. Runs on the real ~/.hermes; a user-set HERMES_HOME survives to the child (custom-home users keep working). All overlay machinery removed.
  • httpx resolves HTTPS_PROXY for both schemes: https upstreams ride CONNECT + cert MITM, http upstreams ride absolute-form forward-proxy requests (server side added: resolveUpstream absolute-form block, self-host guarded).
  • parseHermesYaml now accepts base_url / url / api with hermes' own priority (base_url > url > api) — hermes' canonical form is base_url:; the old reader matched api: only.

omp (phase 3) — reuses the pi manifest

  • buildPiEnv BILI_PROVIDER_REWRITES manifest shared with pi; PI_CODING_AGENT_DIR stripped from discovery + child env; -e dist/agent/omp.js when the plugin is not installed; pre-spawn refusal parity before the proxy starts.
  • omp stale-model bypass fixed: omp pins the session Model from a pre-extension snapshot — registerProvider rewrites the registry + runtime overrides but never refreshes agent.state.model (omp lacks pi's _refreshCurrentModelFromRegistry), so streaming keeps the raw baseUrl and fetches go direct, bypassing the proxy. Fix: on session_start / session_switch the plugin reads ctx.model and calls setModel({...model, baseUrl: rewritten}) to re-pin. Verified end-to-end: one-shot omp -p now shows forward POST → :8197/v1/responses in the proxy log (was zero forwards before).

MITM cert strictness (found by the hermes smoke)

Python/httpx (OpenSSL 3 strict chain verification) rejected our generated certs where curl tolerated them:

  1. certificate verify failed: Missing Authority Key Identifier — leaves had no AKI/SKI
  2. Basic Constraints of CA cert not marked critical — root BC not critical

Fixed in src/ca.ts: leaves carry SKI + AKI (= root SKI, explicit bytes), root basicConstraints is critical. mintHostCert() extracted for testability. Note: existing installs regenerate their root CA on next start (10-year self-signed root; path-referenced trust bundles like HERMES_CA_BUNDLE / NODE_EXTRA_CA_CERTS pick the new one up automatically). Smoke-verified against real open.bigmodel.cn MITM via hermes + python httpx.

Phase 4 (dsh) — deferred

dsh's fetch bypasses proxy env vars for loopback upstreams; recorded on #535 for later.

Pre-flight

  • npm run typecheck clean
  • npm test 1018/1020 — the 2 fails are the known env-dependent plugin-agent tests (live BILLION_CONTEXT_PROXY leaks into the test process; pass 32/32 standalone)
  • npm run build OK
  • Smokes: hermes one-shot http → absolute-form forward ✓; hermes one-shot https (bigmodel) → MITM tunnel + forward, upstream 401 with bogus key as expected ✓; omp one-shot → forward POST ✓; real ~/.hermes/config.yaml + ~/.omp/agent/* mtimes untouched ✓

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 Reviewing #538 now — checked out facf9fd (stacked on #536 head 03f7753). Auditing the incremental diff (03f7753..facf9fd) against the #535 zero-file design and re-running typecheck/test/build locally to confirm the pre-flight claims. Focusing on: hermes env-only routing + the new absolute-form forward path, omp setModel repin correctness, and the MITM cert changes (AKI/SKI + critical root BC). Will follow up with findings.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 Review complete — audited the incremental diff (03f7753..facf9fd, stacked on #536) and re-ran the full pre-flight locally. Verdict: core logic is correct and the security properties hold; two deliberate tradeoffs worth acknowledging before landing; rest are nits.

Pre-flight (re-run on facf9fd)

  • npm run typecheck — clean ✓
  • npm run build — OK ✓
  • npm test — 1019/1020 locally. The single failure is not a PR regression: it's the pre-existing resolveClientCommand: codex/claude resolve to themselves (tests/launcher.test.ts:975), which assumes the PATH dir has no codex/claude. On any box where those CLIs are installed under it, resolveClientCommand correctly returns the absolute path and the assertion (expecting the bare name) fails. It's present unchanged at the feat: pi file-free injection (#535 phase 1) #536 head and the PR doesn't touch resolveClientCommand. Your "1018/1020 (2 env-dependent plugin-agent fails)" and my "1019/1020 (1 resolveClientCommand fail)" are two faces of the same coin — different env-sensitive tests trip on different machines (here the plugin-agent tests pass because there's no BILLION_CONTEXT_PROXY leak). Net: no test regression introduced by this PR. Side note: that test is latently flaky on dev boxes with codex/claude installed — it should use a synthetic PATH dir like the pi case at :993 does. Minor, out of scope here.

Security — new absolute-form forward path (src/server.ts:301): verified safe. It returns { tunnel: true }, identical shape to the /bili/ embedded form, and the #409 destination admission (src/server.ts:848) keys on route?.tunnel — so self-proxy / link-local / metadata are always denied and private/loopback denied for remote clients unless allowlisted, exactly as for /bili/. No new SSRF surface. The self-host loop guard (u.host !== req.headers.host) is correct and covered by tests (incl. malformed + origin-form fallthrough).

MITM cert strictness (src/ca.ts): correct. Root basicConstraints now critical: true (fixes OpenSSL 3 "BC not marked critical"); leaves gain SKI + an AKI whose keyIdentifier is explicitly the root's SKI bytes (rootCert.generateSubjectKeyIdentifier().getBytes()) — the inline note correctly flags that forge's true shorthand would stamp the leaf's own SKI. Fixes "Missing Authority Key Identifier." mintHostCert() cleanly extracted, per-host caching preserved in getSecureContext. New mitm.test.ts asserts critical BC + AKI-carries-root-SKI + root.verify(leaf). One-time root-CA regen on existing installs is handled (path-referenced bundles pick it up).

omp stale-model bypass (src/agent/pi.ts): correct, well-gated. repin() reads ctx.model.provider, looks it up in the BILI_PROVIDER_REWRITES manifest, skips when already rewritten (idempotent), and calls setModel({...model, baseUrl: rewritten}) on session_start/session_switch, handling both setModel→false (no API key) and throw. Gated on agent==="omp" && rewrites && typeof pi.setModel==="function". Matches your e2e trace (forwards now appear). console.error here is fine — this file bundles to client-side dist/agent/omp.js, not a server module.

buildPiEnv / manifest sharing (src/launcher.ts:343): correct. One {key: wrappedUrl} manifest from httpRewrites; both pi (:1715) and omp (:1738) pass routes.httpRewrites. The third call site (runTestPi, :1910) omits it deliberately — that path runs --no-extensions and never consumes the manifest. Adding no_proxy/NO_PROXY to INHERITED_PROXY_VARS is the right call (an inherited exclusion list could otherwise exempt the proxy host itself).

parseHermesYaml (src/client-config.ts:388): dict form now honors hermes' base_url > url > api priority via the null-byte-keyed candidates map. Minor: the legacy custom_providers: list branch still last-writer-wins (:451/:454) rather than applying the same priority — only bites if a legacy-list entry carries multiple conflicting keys ordered against hermes' own resolution. Edge case, pre-existing for that branch; noting for completeness.

Two deliberate tradeoffs (both intentional + commented — putting them on the record):

  1. hermes loses explicit session identity. Dropping writeHermesIdentityPlugin makes hermes requests anonymous, binding via the # [Bug/Regression] 0.1.59 关闭内容指纹会话兜底 (#286):无会话头第三方 harness 请求被直接 400,不转发上游 #309 content-prefix-affinity fallback (src/server.ts:1150-1173) instead of prompt_cache_key. That's the price of true zero-file (a Python plugin in the user's real ~/.hermes/plugins/ would violate never-write-to-user-home). Consequence: two hermes conversations sharing an opener can collide onto one session — self-healing (no data loss, worst-case reduced compression efficiency) but a real isolation step-down vs the 注入零文件化:废除 overlay/临时目录机制(#531 设计返工) #535 sketch. Acceptable under the mandate; just confirming it's a conscious choice.
  2. omp cancels ALL native compaction under bili (auto and manual) vs pi's auto-only, because omp's session_before_compact has no reason. Correct consequence of that gap; "use /acp instead" is sound. Note the pi/omp asymmetry is user-visible.

Nits (non-blocking):

Process: stacked on #536 — needs #536 merged first (GitHub retargets to master on base delete). README bili omp/bili hermes lines updated accurately. version untouched (correct for a content PR). No as any/@ts-ignore; no new console.error in server modules.

👍 from me once #536 lands — subject to acknowledging the two tradeoffs above.

@ranxianglei
ranxianglei force-pushed the 2026-09-04_535-hermes-omp-file-free branch from facf9fd to 8a86fd3 Compare September 5, 2026 01:55
@ranxianglei
ranxianglei force-pushed the 2026-09-04_535-hermes-omp-file-free branch from 8a86fd3 to 038cda5 Compare September 5, 2026 02:02
…ctness fix

hermes (#535 phase 2):
- launcher: env-only routing — stripInheritedProxy + HTTPS_PROXY=proxy +
  HERMES_CA_BUNDLE=combined bundle; runs on the REAL ~/.hermes (a user-set
  HERMES_HOME survives to the child); overlay/HERMES_HOME redirect removed.
  httpx resolves HTTPS_PROXY for both schemes: https upstreams ride
  CONNECT + cert MITM, http upstreams ride absolute-form forward-proxy
  requests the server understands. No identity plugin.
- client-config: parseHermesYaml accepts base_url/url/api (hermes priority
  base_url > url > api) — the canonical form is base_url, old reader
  matched api only.

omp (#535 phase 3):
- launcher: buildPiEnv BILI_PROVIDER_REWRITES manifest (shared with pi),
  PI_CODING_AGENT_DIR stripped from discovery + child, -e dist/agent/omp.js
  when the plugin is not installed, refusal parity before proxy start.
- agent/pi.ts: omp session_start/session_switch repin — omp pins the
  session Model from a pre-extension snapshot, so registerProvider
  rewrites never reach agent.state.model and streaming keeps the raw
  baseUrl (direct fetch, bypasses the proxy). setModel({...model,
  baseUrl: rewritten}) re-pins it; omp lacks pi's
  _refreshCurrentModelFromRegistry, this is the client-side equivalent.

server: absolute-form forward-proxy requests (GET http://upstream/path via
the proxy) — self-host guarded, routed upstream with tunnel semantics.

ca: strict OpenSSL 3 (python/httpx) compatibility — leaf certs now carry
SKI + AKI (root SKI, explicit bytes) and the root's basicConstraints is
critical; mintHostCert extracted for testability. hermes/httpx rejected
the old certs with "certificate verify failed: Missing Authority Key
Identifier" / "Basic Constraints of CA cert not marked critical" while
curl tolerated them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant