Skip to content

feat(plugins): add OMNIROUTE_PLUGINS_DIR to override the plugin scan directory (#11827) - #11906

Merged
diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.51from
pacocartones:feat/plugins-dir-env-override-11827
Aug 30, 2026
Merged

diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.51from
pacocartones:feat/plugins-dir-env-override-11827

Conversation

@pacocartones

@pacocartones pacocartones commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds OMNIROUTE_PLUGINS_DIR, an explicit override for the directory the runtime plugin
scanner
reads, consulted before the home-derived default (src/lib/plugins/scanner.ts →
getDefaultPluginDir()).

Today that directory comes only from HOME / USERPROFILE, with a silent fallback to
/tmp/.omniroute/plugins. An image that never exports HOME — the reporter's case, and the
default for several base images — therefore scans a path nobody mounted, and the only trace is
an info-level scanner.dir_not_found that never names HOME as the cause. The bind-mounted
plugin tree is simply invisible, and the workaround (setting HOME in compose to match the
mount) moves every home-relative behaviour to fix one of them.

  • OMNIROUTE_PLUGINS_DIR=/opt/omniroute/plugins → the scanner reads that path, whatever HOME says.
  • Unset → byte-for-byte today's behaviour (<home>/.omniroute/plugins, /tmp/... with no home).
  • A blank/whitespace value counts as unset, so an empty OMNIROUTE_PLUGINS_DIR= in a compose
    file cannot point the scanner at a nameless path.
  • The resolved directory is logged once at startup as scanner.dir_resolved, naming the input
    that won (OMNIROUTE_PLUGINS_DIR / home / no-home-fallback) — the reporter's "bonus" item,
    and what turns this failure class from silent into obvious.
  • It is also the root PluginManager installs into (manager.ts:111 takes its pluginDir from
    the same function), so an override moves discovery and installation together rather than
    splitting them across two directories.

Not a duplicate of OMNIROUTE_PLUGIN_PATH

The repo already has a similarly-named variable, and it is a different subsystem — worth stating
explicitly so this doesn't read as a redundant knob:

OMNIROUTE_PLUGIN_PATH (existing) OMNIROUTE_PLUGINS_DIR (this PR)
Read by bin/cli/plugins.mjs → getPluginDirs() src/lib/plugins/scanner.ts → getDefaultPluginDir()
Finds omniroute-cmd-* npm packages that add omniroute subcommands (package.json, register()) server-side plugin.json plugins run inside the proxy (manifest, hooks, DB registration)
Consumer the CLI process the server: POST /api/plugins/scan, pluginManager.loadAll() / install()
Semantics appended to ~/.omniroute/plugins — an extra search path replaces the home-derived scan root

Setting one has never affected the other, and after this PR it still doesn't. .env.example,
docs/reference/ENVIRONMENT.md, docs/frameworks/PLUGINS.md and
docs/frameworks/PLUGIN_MARKETPLACE.md now cross-reference each other so the next reader doesn't
have to rediscover the split.

Related Issues

Validation

  • Change type: docker/deployment + plugins runtime (config resolution, no route/DB/UI surface)
  • Focused tests and category gates from the golden path
  • npm run lint (eslint on the touched TS files: clean; prettier --check clean; lint-staged ran on commit)
  • Reconciled with the current active release base — branched from release/v3.8.51 at 13afbfa, the branch tip at the time of writing
  • Production-code changes include new automated tests in this PR
  • Changelog fragment: changelog.d/features/11906-omniroute-plugins-dir.md (check:changelog-integrity OK — no base bullets lost vs origin/release/v3.8.51)

Failing-then-passing (Hard Rule #18) — same committed test file, source toggled:

# scanner.ts at the parent commit, tests from this PR
ℹ tests 12 · pass 9 · fail 3
  ✖ honors OMNIROUTE_PLUGINS_DIR regardless of HOME
  ✖ honors OMNIROUTE_PLUGINS_DIR when the process has no HOME at all
  ✖ makes the scanner read the overridden directory (Docker bind-mount case)

# with this PR's scanner.ts
ℹ tests 12 · pass 12 · fail 0

Wider plugin surface, all 46 tests/unit/*plugin*.test.ts files: 486 pass / 0 fail.

Gates that depend on this change:

  • npm run check:env-doc-sync — the mandatory one for a new env var. It was red on the source
    change alone
    (In code but missing from .env.example: OMNIROUTE_PLUGINS_DIR) and is green
    with .env.example + docs/reference/ENVIRONMENT.md updated: ✓ Env / docs contract is in sync.
  • npm run check:doc-links — PASS (859 internal links).
  • check:docs-sync, check:any-budget:t11, check:tracked-artifacts — PASS (pre-commit hook).
  • check:docs-symbols, check:test-discovery, check:file-size — PASS.
  • tsc -p over the two changed TS files — clean.

Gates already red on release/v3.8.51 before this branch (verified by running each on a
pristine checkout of 13afbfa — identical output there and here, and none of them names a file
this PR touches):

  • check:docs-counts — 11 strict drifts: provider counts (351 vs 357 in README.md, AGENTS.md,
    llm.txt, package.json, 4 SVGs) and migration counts (165 vs 160).
  • check:fabricated-docs --strict — 1 drift: docs/providers/CHATGPT_WEB.md:129 references
    tests/unit/migration-163-retire-chatgpt-web.test.ts, which does not exist.
  • check:mutation-test-coverage --strict — 5 covering tests missing from stryker.conf.json
    across open-sse/utils/error.ts and the *-web-runtime-block modules. scanner.ts is not in
    the mutate set.
  • typecheck:core — pre-existing TS2345 in open-sse/executors/antigravity/executeAttempt.ts:392.

Tests Added Or Updated

tests/unit/plugins-scanner.test.ts — six new cases in the getDefaultPluginDir block, each
pinning OMNIROUTE_PLUGINS_DIR / HOME / USERPROFILE through a save-and-restore helper so no
case depends on (or leaks into) the runner's ambient environment:

  1. override wins over a conflicting HOME;
  2. override honoured with no HOME at all (the Docker case);
  3. unchanged home-derived default when the override is unset (regression guard);
  4. blank override treated as unset;
  5. /tmp/.omniroute/plugins fallback with neither input set;
  6. end-to-end: scanPluginDir(getDefaultPluginDir()) discovers a plugin in a directory named
    only by the variable.

The pre-existing "returns a string path" case is kept as-is.

Coverage Notes

src/lib/plugins/scanner.ts is the only production file changed, and every branch of the new
resolution (env / home / no-home fallback / blank-override) is asserted by the tests above —
coverage on the touched function goes up, not down. Docs and .env.example carry no coverage.

Reviewer Notes

  • No migration, no flag, no route change. With the variable unset the resolved path is
    identical to today's, which is what case 3 pins.
  • The override is used verbatim (not resolve()d). The manager's assertWithinPluginDir() /
    assertEntryPointWithinDest() guards already resolve() both sides before comparing, so a
    relative or non-normalised override cannot weaken the CRITICAL-2/-3 containment checks.
  • docker-compose.yml needs no change: it uses env_file: .env, so the variable reaches the
    container as soon as an operator sets it. docs/guides/DOCKER_GUIDE.md gains one row in
    Critical Environment Variables (width-matched to the existing table so prettier leaves the
    other rows untouched).
  • docs/reference/ENVIRONMENT.md is in .prettierignore (manually aligned); the new row follows
    the surrounding column padding of section 2.
  • Reported by @amaleta in [Feature] OMNIROUTE_PLUGINS_DIR — explicit env override for the plugin scan directory #11827; the acceptance criteria there are covered point by point
    (override honoured regardless of HOME, unchanged when unset, resolved directory logged at
    startup, Docker docs mention it, unit coverage on both branches).

@pacocartones

Copy link
Copy Markdown
Contributor Author

CI note, so the red mark isn't mistaken for this PR's doing:

Docs Gates (fast-path) fails on a drift that predates this branch. The job runs
check:docs-all, whose second step is check:docs-counts — it reports the same 11 strict
drifts on a pristine checkout of 13afbfa (the release/v3.8.51 tip this branch was cut
from) as it does here: provider count 351 vs the 357 written in README.md, AGENTS.md,
llm.txt, package.json and 4 SVGs, plus migrations 165 vs the 160 in the same three files.
None of them names a file this PR touches, and fixing them would mean rewriting counts across
README/AGENTS/llm.txt/package.json/diagrams — a separate change, and one that belongs
to whoever regenerates those.

Because check:docs-all is an && chain, that failure short-circuits before
check:env-doc-sync
— the gate that actually governs this PR. Run on its own it is green
with the change in place:

$ npm run check:env-doc-sync
  ✓ In code but missing from .env.example: none
  ✓ In .env.example but missing from ENVIRONMENT.md: none
  ✓ In ENVIRONMENT.md but missing from .env.example: none
✓ Env / docs contract is in sync.

and it was red on the source change alone (In code but missing from .env.example: OMNIROUTE_PLUGINS_DIR) until .env.example and docs/reference/ENVIRONMENT.md were updated
— which is what the gate is there to force.

npm run check:doc-links also passes (859 internal links), and check:docs-sync passed in the
same job before the counts step.

@pacocartones

Copy link
Copy Markdown
Contributor Author

Second red mark, same story: No new ESLint warnings fails on the branch, not on this diff.

The job's error is not a lint violation — it is ESLint's suppressions bookkeeping:

There are suppressions left that do not occur anymore.
Consider re-running the command with `--prune-suppressions`.
##[error]Process completed with exit code 2.

Byte-identical failure on #11875 (different author, different subsystem, five hours earlier
on the same base), so config/quality/eslint-suppressions.json has entries that no longer
match the tree — a branch-level cleanup (--prune-suppressions), not something a feature PR
can fix from its own files.

This diff cannot be the cause: neither src/lib/plugins/scanner.ts nor
tests/unit/plugins-scanner.test.ts appears anywhere in the 1234 suppression entries, so
nothing here can turn a suppressed violation stale. Linting the two changed files directly
with the same suppressions file is clean:

$ npx eslint src/lib/plugins/scanner.ts tests/unit/plugins-scanner.test.ts \
    --suppressions-location config/quality/eslint-suppressions.json
$ echo $?
0

prettier --check is clean on both, and tsc over the two files reports nothing.

Happy to rebase once either gate is unblocked on release/v3.8.51.

@pacocartones

Copy link
Copy Markdown
Contributor Author

Full CI picture now that every job has reported — the check matrix on this PR is identical,
job for job, to #11875
(different author, different subsystem, same release/v3.8.51 base):

Check #11906 (this PR) #11875
Change Classification pass pass
Merge integrity (changelog + generated skills) pass pass
Vitest (fast-path) pass pass
semgrep · semgrep-cloud-platform pass pass
Docs Gates (fast-path) fail fail
Fast Quality Gates fail fail
No new ESLint warnings fail fail
Unit Tests fast-path 1/4 · 2/4 · 3/4 · 4/4 fail fail
Build (advisory) fail fail
dast-smoke fail fail

The unit-shard failures are the same doc/asset/dependency gates, reproduced on a pristine
checkout of 13afbfa
(this branch's base, no changes applied) — 9 failures, identical names:

✖ 6A.8: all workspace package deps are in the allowlist            (check-deps)
✖ the gate exits 0 against the current (synced) repo state         (check-docs-counts-sync)
✖ provider asset provenance gate … (×3) + audited 142-file snapshot (check-provider-asset-provenance)
✖ g4f.space sub-providers no longer advertise an anonymous free tier
✖ no first-party TypeScript module is imported through a .js specifier
✖ #7253 release-green: docs contain zero fabricated API/file-ref drift

plus, in shard 4, check-fabricated-docs (the CHATGPT_WEB.md:129 reference from the earlier
comment) and token-health-check-webcookie — both also failing on the pristine base here.
dast-smoke died on infrastructure: "The runner has received a shutdown signal."

Not one of them names a file this PR touches. What this PR's own change is answerable for is
green:

  • check:env-doc-sync — the gate a new env var must satisfy: red on the code change alone,
    green once .env.example + docs/reference/ENVIRONMENT.md were updated.
  • All 46 tests/unit/*plugin*.test.ts files — 486 pass / 0 fail, including the 6 new cases.
  • check:doc-links, check:docs-sync, check:docs-symbols, check:test-discovery,
    check:file-size, check:changelog-integrity — pass.
  • eslint + prettier + tsc on the two changed TypeScript files — clean.

No action requested; flagging it so the red column isn't read as this diff's doing. I'll rebase
and re-run whenever the base is green.

…directory (cheaper-inference#11827)

The runtime plugin scanner resolved its directory only from HOME/USERPROFILE,
with a silent fallback to /tmp/.omniroute/plugins. A container image that does
not export HOME therefore scans a path nobody mounted, and the only trace is an
info-level scanner.dir_not_found that never names HOME as the cause, so the
bind-mounted plugin tree is simply invisible.

getDefaultPluginDir() now consults OMNIROUTE_PLUGINS_DIR first and logs the
directory it settled on (scanner.dir_resolved) together with the input that won:
OMNIROUTE_PLUGINS_DIR, home, or no-home-fallback. Behaviour is unchanged when the
variable is unset, and a blank value counts as unset so an empty compose entry
cannot point the scanner at a nameless path. The same directory is the root
PluginManager installs into, so an override moves discovery and installation
together.

This is not OMNIROUTE_PLUGIN_PATH: that variable is read only by the CLI
command-plugin loader (bin/cli/plugins.mjs) to find omniroute-cmd-* packages and
has never reached the server-side scanner. Both docs now say so explicitly.

tests/unit/plugins-scanner.test.ts gains six cases: override wins over HOME,
override with no HOME at all, unchanged home-derived default, blank override
ignored, /tmp fallback, and an end-to-end scan of a directory named only by the
variable. The three asserting the new behaviour fail on the parent commit.
@pacocartones
pacocartones force-pushed the feat/plugins-dir-env-override-11827 branch from 65db405 to 728f3a6 Compare August 29, 2026 02:51
@diegosouzapw
diegosouzapw merged commit 2471a0d into cheaper-inference:release/v3.8.51 Aug 30, 2026
14 of 15 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…directory (cheaper-inference#11827) (cheaper-inference#11906)

Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Clean, well-scoped env-override with correct blank-value handling and a startup log naming the resolution source. Thanks.
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.

[Feature] OMNIROUTE_PLUGINS_DIR — explicit env override for the plugin scan directory

2 participants