Repository navigation
Description
Summary
bin/check-opencode-metered-providers.mjs — the guard that fails CI/local checks when a metered (PAYG) provider (deepseek, cloudflare-*) is selected without an adjacent // payg-exception: marker — silently no-ops when invoked through any symlink. Its entry-point check at line 359 (origin/main 03b5817dc) is:
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main();
}path.resolve() only normalizes the invoked path; it does not resolve symlinks, while fileURLToPath(import.meta.url) is the module's real path. Invoked via a symlinked file or through a symlinked directory (e.g. the fleet's own ~/code -> ~/git/code checkout alias that caused #2083), the two never match, main() never runs, and the process exits 0 with no output — a metered selection that should fail the guard reports as passing.
This is the same bug class as #2083 / #2235 / #4325, in a fifth file and in a different syntactic shape (reversed comparison, resolve() instead of pathToFileURL(...).href), so the repo-wide grep in #4325's acceptance criteria — written against the pathToFileURL shape — does not catch it. A grep for fileURLToPath(import.meta.url) entry-point comparisons in bin/ at origin/main shows this is the only remaining instance in that shape (the other hits are dirname(...) path derivations, test helpers, or the already-fixed realpathSync comparison in bin/provider-catalog-sync.mjs:936).
Evidence (laptop probe ~/git/fleet/probe-2026-11-02-laneB-meteredentry/, file extracted verbatim from origin/main 03b5817dc, md5 72912190ea823ae65c7ef6898f9b64fb both sides)
Config under test: { "model": "deepseek/deepseek-chat" } (a metered selection with no payg-exception marker).
- Direct:
node real/check.mjs config.jsonc→ stderrMetered-provider config guard failed … metered provider "deepseek" selected by $.model …, exit 1 — the guard ran and failed the config. - Via a file symlink:
node link.mjs config.jsonc→ no output, exit 0 — the guard never ran; the metered selection silently "passes". - Via a symlinked directory:
node linkdir/check.mjs config.jsonc→ no output, exit 0 — same silent no-op. - Control, clean config direct (
meta/…model): printsMetered-provider config guard passed, exit 0 — the direct path works as designed.
Why it matters
This guard is the automated backstop for the house rule that PAYG providers are a reserve, never a routine route. A check that exits 0 without checking anything is worse than no check: any wrapper, symlink farm, or checkout-alias invocation (the exact layout that already burned the fleet in #2083) turns a failing metered selection into a green pass, and nothing anywhere records that the guard was skipped.
Distinct from #4325 (open): that issue names four files (bin/codedb-bridge.mjs, the supabase/cloudflare exporters, the claimant installer), all in the pathToFileURL(process.argv[1]).href shape. This file is not named there and its reversed resolve(...) === fileURLToPath(...) shape evades that issue's grep-based acceptance criterion; it needs its own fix (or an explicit addition to #4325's scope plus a widened grep).
Acceptance criteria
-
bin/check-opencode-metered-providers.mjsrunsmain()when invoked via a symlinked file path and via a symlinked directory path (use thefs.realpathSync-on-both-sides comparison proven inbin/proxy-supervise.mjsand applied in fix(bin): run main when provider-catalog-sync and record are invoked via symlink (#2235) #4123, or an equivalent). - Importing the module as a library (its test suite does this) still does not run
main(). - A regression test spawns the guard via a symlink against a metered config and asserts exit 1 with the failure message (and via a symlink against a clean config, exit 0 with the pass message).
- The repo-wide entry-point grep is widened to catch both shapes (
pathToFileURL(argv[1])andresolve(argv[1]) === fileURLToPath(import.meta.url)), and bin+pm-stack: four entry points still use the pre-#2083 raw-path entry-point check — codedb-bridge, supabase/cloudflare exporters and the claimant installer silently no-op via any symlink #4325's acceptance grep is updated or cross-linked so this shape cannot survive that sweep either.
Related: #2083 (original instance + proven fix pattern), #2235 (closed by #4123), #4325 (open — the other four instances, different shape), #2350 (open — meta-exporter instance).
Found by new-issue-creator-code-b (lane B) this run.
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · state: filed
dezren39 commentedon Nov 3, 2026
agent-note:v1
event: picked_up
task: 6673 (repo-health-code-pilot, lane 8ac61616-3815-4e7b-b33d-840a3829154a)
state: claimed via atomic queue claim at 2026-11-02 19:02 CST; issue #4694 is OPEN with no hard-stop labels. Search found no PR referencing #4694; #4668 is Drew-authored and changes only the unrelated opencode-config-guard workflow, so it will not be touched.
next: verify the reported symlink no-op against fresh main, then route the narrowly scoped fix and regression tests without duplicating #4325.
dezren39 commentedon Nov 3, 2026
agent-note:v1
event: done
task: 6673 (repo-health-code-pilot, lane 8ac61616-3815-4e7b-b33d-840a3829154a)
state: triaged; reproduced and routed as follow-up task 6674 (small_fix).
evidence (fresh origin/main 9dfc29e, 2026-11-03): direct invocation on deepseek/deepseek-chat exits 1 with the metered-provider failure; file-symlink and symlinked-directory invocations both exit 0 silently; direct clean-config control exits 0 with pass output.
related work: PR #4583 for #4325 is OPEN and changes four other entry points, not this guard. Cross-linking #4694 into #4325 for visibility; follow-up scope stays separate.
next: task 6674 changes only this guard and its regression tests; #4694 remains OPEN pending that fix.
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
Merge pull request #4697 from developing-today/queue/6674-4694-metere…
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done — PR #4697 merged, issue #4694 closed
realpathSyncon both sides (fail-closed to not-main on resolution error), so symlinked invocation runs the guard and library import stays inert.node --test bin/check-opencode-metered-providers.test.mjs— 17/17 pass on the laptop (13 existing + 4 new: file-symlink metered/clean, directory-symlink metered, import-inert).