Skip to content

fix(service): keep shell-local multishell paths out of service launchers - #6168

Merged
lidge-jun merged 2 commits into
devfrom
codex/rt5-platform-service-launcher-multishell
Sep 28, 2026
Merged

lidge-jun merged 2 commits into
devfrom
codex/rt5-platform-service-launcher-multishell

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Reimplements the service-launcher part of #6030 by @andrew05060414 on current dev, with the fixes from the maintainer review.

fnm, nvm, volta, mise and asdf can put a shell-local *_multishells directory first on PATH. That directory disappears when the installing shell exits. ocx service install or ocx service repair run from such a shell could bake it into the service as its launcher or into its PATH, and the service then failed to start after logout or reboot. This PR:

  • adds isTransientServiceLauncherPath and filterTransientServicePath in src/service/state.ts. They match a path component named (asdf|fnm|mise|nvm|volta)_multishell(s), including Windows spellings.
  • skips transient directories in stableLauncherEntry, both for the recorded launcher and during the PATH scan, and filters them out of the PATH rendered into the systemd unit, the launchd plist, and the Windows service script. The package-local Bun runtime and every non-PATH variable are unchanged.
  • filters the native WinSW service XML the same way, including XML regenerated by native repair (Codex review). The pure filter lives in src/lib/transient-service-path.ts to avoid an import cycle.
  • makes ocx service status and diagnostics report a recorded temporary launcher as stale, and has repair say it is replacing it. The message no longer tells the user to run ocx service repair while repair is already running.

Fixes for the review on the #6030 head (740b68da54):

  • Non-PATH plist changes could vanish. reusePreviousPlistPathVariable now compares the whole normalized plist. It reuses the loaded job's PATH only when PATH is the sole difference, so a changed HTTPS_PROXY (or any other field) always installs the new definition.
  • The live launchd job kept the stale PATH. A cleaned PATH changes the plist bytes, so repair goes through the existing guarded bootout/bootstrap path and reports reloaded: true. Tests assert both the bytes and the launchctl verbs for the cleanup-only case and for cleanup combined with a proxy change.

This PR leaves out the Codex shim and ocx connect readiness changes from #6030. The review found that the shim part could wrap an unresolved temporary target, and it would also exceed the file-size caps of src/codex/shim.ts and tests/codex-integration/codex-shim.test.ts. It should be reviewed separately.

Supersedes the launcher part of #6030.

Co-authored-by: Andrew shiyuanwang@ucsb.edu

Verification

  • bun test tests/service/service-multishell-path.test.ts (new): 7 pass. tests/service/launchd-repair.test.ts: 60 pass. tests/service/service.test.ts: 210 pass. tests/service/systemd-install-cleanup-hardening.test.ts: 1 pass.

  • Other suites that render or diagnose service definitions: bun test tests/codex-integration/codex-service-manager-probe.test.ts tests/codex-integration/doctor.test.ts tests/service/autostart-health.test.ts tests/service/service-ownership-handover.test.ts tests/service/service-wsl-home-ownership.test.ts tests/service/standalone-service.test.ts tests/windows/windows-service-wrappers.test.ts: 211 pass, 1 skip, 0 fail.

  • bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts: 0 fail.

  • bun run typecheck, bun run structure:check, bun run privacy:scan: pass.

  • Still open: a real launchd and systemd lifecycle on a machine where the service was installed from an fnm shell, with the shell closed afterwards. The maintainer asked for this proof, and unit tests do not provide it.

  • Local tests after the review round were skipped by maintainer instruction (no local bun test/typecheck/test:changed). The GitHub PR CI on the current head is the verification evidence for the latest commits. The local results above predate that instruction.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. (docs-site/.../getting-started/installation.md, structure/ops/service-and-sidecars.md, and structure/runtime.md)
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. The change only narrows which executable and PATH entries a service may bake in. It adds no tokens, credentials or workflow changes.

…efinitions

Normalize and compare full launchd plists before PATH reuse, then reload changed definitions through the existing guarded lifecycle. Preserve bundled Bun and non-PATH settings; report temporary launcher repair in the present tense.

Co-authored-by: Andrew <shiyuanwang@ucsb.edu>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 28, 2026 08:02
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T08:05:56.286965Z 8325639 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: c0d13a7c-3286-42c3-b6bd-1072e4805936

📥 Commits

Reviewing files that changed from the base of the PR and between cbe0d40 and 8325639.

📒 Files selected for processing (13)
  • docs-site/src/content/docs/getting-started/installation.md
  • scripts/test-layout/layout.json
  • src/service/diagnostics.ts
  • src/service/launchd.ts
  • src/service/repair.ts
  • src/service/state.ts
  • src/service/systemd.ts
  • src/service/windows-taskxml.ts
  • structure/ops/service-and-sidecars.md
  • structure/runtime.md
  • tests/fixtures/test-layout-expected.json
  • tests/service/launchd-repair.test.ts
  • tests/service/service-multishell-path.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

Service launcher selection and generated service definitions now exclude shell-local multishell paths. Repair reports stale recorded launchers and reloads launchd when plist content changes.

Changes

Service path handling

Layer / File(s) Summary
Recognize transient paths and select launchers
src/service/state.ts, tests/service/service-multishell-path.test.ts
The service recognizes multishell paths for asdf, fnm, mise, nvm, and volta. Launcher selection skips transient and non-absolute PATH entries, using platform-specific path rules. Tests cover durable launcher selection and fallback to bundled Bun.
Filter service definitions and normalize launchd repair
src/service/systemd.ts, src/service/launchd.ts, src/service/windows-taskxml.ts, tests/service/service-multishell-path.test.ts, tests/service/launchd-repair.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json, structure/ops/service-and-sidecars.md
Systemd, launchd, and Windows definitions remove transient entries from PATH. Launchd repair compares plist content after PATH normalization. Tests cover reloading after stale PATH cleanup and retaining other environment settings.
Report and repair stale launchers
src/service/diagnostics.ts, src/service/repair.ts, tests/service/service-multishell-path.test.ts, docs-site/src/content/docs/getting-started/installation.md, structure/runtime.md
Diagnostics identify recorded transient launchers. Repair emits a diagnostic before platform-specific repair. Tests and documentation describe repair guidance and the resulting launcher behavior.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Suggested reviewers: luvs01

Merge Risk: ⚪ Minimal · up to 83256

No identified issue blocks merging. Real launchd and systemd lifecycle testing after the installing shell exits remains open.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 83256

A failure during macOS service repair can make a cleaned service definition appear active while the running service still has its old PATH. The affected scope is the installing user's background service; the review did not establish a new cross-user privilege path.

Retained concerns

  • Medium · security · inferred: A failed eviction followed by a failed bootstrap can leave the old launchd job's transient PATH live while repair reports a reload and records the replacement state.
Security review details

Security Blast Radius

  • inferred — The identified drift concerns the environment of an already installed, per-user launchd job. The evidence does not show a new cross-user capability or an expanded command source.

Security Findings and Attack Paths

  • inferred — If eviction leaves the old job loaded, a failed bootstrap can be followed by a command-matching verdict. Repair can then report success without proving that the live job lost its shell-local PATH. This is a conditional control-drift path, not proof of attacker execution.

Trust Boundaries and Controls

  • observed — The changed launcher-selection control rejects transient, relative, and non-executable candidates. launchd's service command is constructed from the package runtime rather than from the inherited PATH.

Resilience and Maintainability Implications

  • observed — The added PATH-cleanup tests cover successful replacement and preservation of a changed proxy value; they do not establish the live-environment outcome after failed eviction and bootstrap.

Hardening Proposals

  • proposed — For an environment-changing replacement, avoid recording success after a failed bootstrap merely because the old command is still loaded; require evidence that the new definition became live or report the state as unresolved.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 8 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: preventing shell-local multishell paths from being used in service launchers.
Full details: Docstring Coverage

Explanation

Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 8 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8325639ce8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread structure/ops/service-and-sidecars.md Outdated
Comment on lines +72 to +74
Service launchers reject recorded and newly discovered paths inside shell-local `fnm`, `nvm`,
`mise`, `asdf`, or `volta` multishell directories. Systemd, launchd, and Windows definitions
remove those entries from their rendered PATH while keeping other environment values. Launchd

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter the native WinSW service PATH too

When a Windows user installs the opt-in native backend with ocx service install --native, this contract does not hold: buildWinswXml() still writes env.PATH verbatim in src/lib/winsw.ts:109, and native repair regenerates that XML through installWinswService(). Only the Task Scheduler batch script was updated, so a native service installed or repaired from a multishell still retains the disappearing directory and can fail after logout or reboot. Apply the same filtering to the WinSW XML and cover that backend with a regression test.

AGENTS.md reference: structure/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6f4c3943c4: buildWinswXml() now filters the saved Windows PATH through the same transient-directory filter, so native install and native repair (which regenerates the XML through installWinswService()) drop *_multishells entries too. The pure predicate and filter moved to src/lib/transient-service-path.ts to avoid a winsw ↔ service/state import cycle, and state.ts re-exports them. tests/service/service-multishell-path.test.ts adds the WinSW case: a mixed-case transient directory is removed while durable entries and XML escaping are kept. An audit of every service definition that bakes PATH found launchd, systemd and Task Scheduler already filtered.

@github-actions github-actions Bot added the bug Something isn't working label Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@Ingwannu

Copy link
Copy Markdown
Owner

Exact-head source review found no P0-P2 defect: the transient-path classifier, per-platform delimiters, launcher fallback, full-plist comparison, non-PATH preservation, and launchd reload decision are coherent, and all three service-lifecycle jobs are green. I am still holding approval on the explicit acceptance criterion carried from #6030: the current tests mock the transient-PATH repair and launchctl verbs, while the PR itself says install from a real fnm shell → close that shell → repair/restart/reboot has not been exercised. Please attach one disposable real systemd proof and one real launchd proof (or a hosted job that creates the transient PATH, installs, removes it, and verifies the live job after repair). No production/live service was touched during this review.

Co-authored-by: Andrew <shiyuanwang@ucsb.edu>
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 58 / 80

이 PR은 컴퓨터를 껐다 켜면 배경 서비스가 죽던 일을 고칩니다. fnm 같은 도구는 터미널이 열려 있는 동안만 있는 폴더를 PATH 맨 앞에 넣습니다. 이름에 fnm_multishells 같은 말이 들어갑니다. 그 터미널에서 ocx service install이나 ocx service repair를 하면, 예전에는 그 폴더가 서비스 실행 파일이나 PATH로 저장됐습니다. 터미널을 닫거나 재부팅하면 폴더가 없어서 서비스가 시작하지 못했습니다.

이제는 경로의 한 칸이 asdf, fnm, mise, nvm, volta 뒤에 _multishell 또는 _multishells이면 임시 폴더로 봅니다. 대소문자는 가리지 않고 윈도우 경로도 봅니다. 실행 파일을 고를 때 그 폴더는 건너뜁니다. 남는 파일이 없으면 패키지 안의 Bun을 씁니다. systemd, macOS plist, 윈도우 작업 스크립트, WinSW XML에 넣는 PATH에서도 그 폴더를 뺍니다. 패키지 Bun과 PATH가 아닌 설정은 그대로 둡니다.

이미 저장된 실행 파일이 임시 폴더이면 ocx service status가 오래됐다고 알립니다. repair가 그 안내를 낼 때는 지금 바꾸는 중이라고만 하고, repair를 또 실행하라는 말은 빼습니다.

macOS repair는 plist 전체를 비교합니다. PATH만 다르면, 고치는 셸의 짧은 PATH로 바꾸지 않고 이미 돌고 있는 작업의 PATH를 유지합니다. 그 예전 PATH 안의 임시 폴더만 지웁니다. 파일 내용이 바뀌면 launchd를 내렸다가 다시 올립니다. HTTPS_PROXY처럼 다른 값이 바뀌면 새 plist를 통째로 설치합니다. #6030에 있던 Codex shim과 ocx connect 검사는 여기 없습니다. 베이스 브랜치는 dev입니다.

src/service/state.ts serviceLauncherPathDiagnostic - 상태 검사는 저장된 실행 파일만 봅니다. 실행 파일은 패키지 Bun인데 PATH 안에만 임시 폴더가 있는 예전 서비스는 정상으로 나옵니다. 그 PATH는 ocx service repair를 해야 지워집니다. 문서에는 repair하라고 적혀 있지만, status는 이 경우를 집어 주지 않습니다.

src/service/launchd.ts reusePreviousPlistPathVariable - 함수 위 주석은, 이전 PATH를 되돌리면 파일이 같아져서 재시작하지 않는다고 말합니다. 코드는 임시 폴더를 뺀 다음 비교합니다. 그 차이만 있으면 bootout 뒤에 bootstrap 합니다. 주석만 보고 재시작을 빼면, 살아 있는 launchd가 옛 PATH를 계속 쓰는 문제로 돌아갑니다.

메인테이너의 판단이 필요한 지점

머지 전에 실기 확인이 한 번씩 필요합니다. fnm 터미널에서 서비스를 설치하고, 그 터미널을 닫은 다음, repair와 재시작 또는 재부팅 뒤에 서비스가 떠 있는지입니다. 지금 테스트는 launchctl 명령을 흉내 냅니다. PR 본문도 이 확인이 남아 있다고 적었습니다. Linux systemd 하나, macOS launchd 하나면 됩니다.

임시 폴더인지는 이름만으로 정합니다. 오래 쓰는 폴더를 우연히 fnm_multishells라고 부르면 PATH에서 같이 빠집니다. nvm, volta, asdf, mise가 실제로 그 이름을 쓰는지는 이 테스트가 보여 주지 않습니다. fnm의 fnm_multishells는 이 규칙과 맞습니다.

너의 추천

런처를 임시 폴더 밖으로 빼는 방향은 맞습니다. 실기 확인 전에는 승인만 보류하면 됩니다. #6030은 아직 열려 있습니다. 이 PR이 그중 런처 부분을 대체하고, shim과 ocx connect는 빠져 있습니다. #6030 전체를 닫지 말고, 런처 설명은 이 PR로 넘긴 다음 남은 부분만 따로 리뷰하면 됩니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun

Copy link
Copy Markdown
Owner Author

@Ingwannu Understood. The real lifecycle proof is the one remaining item, and this lane can't supply it right now. The maintainers have stopped local test runs and local proxy or service starts for verification in this train, and a hosted job that installs a real systemd unit would need a new workflow change, which crosses the security boundary this PR otherwise stays clear of. I'm leaving this for the coordinator's cross-platform run or a maintainer-run disposable proof. Here is the exact procedure.

systemd (disposable Linux VM or user account with a user manager):

  1. eval "$(fnm env --use-on-cd)" in a new shell, then fnm use <node> so PATH starts with /run/user/$UID/fnm_multishells/<pid>/bin.
  2. From that shell, with this branch's ocx on PATH: ocx service install. Then systemctl --user cat opencodex should show no fnm_multishells entry in Environment=PATH=, and ExecStart should point at a durable launcher or the package-local Bun.
  3. Close the shell, then rm -rf /run/user/$UID/fnm_multishells/<pid> if fnm left it behind.
  4. systemctl --user restart opencodex, then curl -fsS http://127.0.0.1:<port>/healthz. Then run ocx service repair from a fresh multishell and check that the unit's PATH is still filtered and the job restarts healthy.

launchd (disposable macOS account):

  1. Same fnm shell setup, then install with this branch.
  2. plutil -p ~/Library/LaunchAgents/<label>.plist | grep PATH should show no multishell entry.
  3. Take an existing install whose plist carries a multishell PATH (or seed one), run ocx service repair, and check that launchctl print gui/$UID/<label> shows the new PATH in the live job's environment and a fresh PID. The unit tests pin the bootout/bootstrap verbs but can't observe that.
  4. Close the shell, remove the multishell directory, launchctl kickstart -k gui/$UID/<label>, then curl /healthz.

@lidge-jun

Copy link
Copy Markdown
Owner Author

Maintainer integration into dev under the MAINTAINERS.md dev exception (lidge-jun, admin).

Exact head 6f4c3943c47303de417255213fc6817951572f5d: the aggregate ci job passed on this head. enforce-target runs were cancelled or queued behind the runner backlog, not failed on the merits. Codex review findings on this PR are fixed on this head. No maintainer change requests are outstanding. Carries #6030 by @andrew05060414 with a Co-authored-by trailer.

@lidge-jun
lidge-jun merged commit 2d5228e into dev Sep 28, 2026
65 of 69 checks passed
@lidge-jun
lidge-jun deleted the codex/rt5-platform-service-launcher-multishell branch September 28, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants