Skip to content

fix(config): sanitize argv before merging into resolved config - #2002

Open
gfargo-horizon-agent[bot] wants to merge 3 commits into
mainfrom
agent/coco-1637-coco-1922-lib-2-fix-config-the-argv-spre
Open

fix(config): sanitize argv before merging into resolved config#2002
gfargo-horizon-agent[bot] wants to merge 3 commits into
mainfrom
agent/coco-1637-coco-1922-lib-2-fix-config-the-argv-spre

Conversation

@gfargo-horizon-agent

@gfargo-horizon-agent gfargo-horizon-agent Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What

Hardens the final argv merge in loadConfig so it can't silently clobber resolved config or drop parts of a merged service object.

Why

Closes #1922
Plane: OSS-1637

The env and git-config loaders both sanitize themselves (removeUndefined), but the trailing { ...config, ...argv } spread in loadConfig did not — so any undefined-valued argv key overwrote a resolved value with undefined, the spread shallow-replaced an argv-supplied service object entirely, and yargs' _/$0 bookkeeping keys leaked into the returned Config. Currently latent (today's callers never emit undefined argv keys), but the fix removes the landmine for any future caller (e.g. hand-built synthetic argv) that does.

How

  • Strip yargs bookkeeping (_, $0) out of argv before merging.
  • Run removeUndefined on the remaining argv so unset flags never clobber a resolved value.
  • Deep-merge service one level, mirroring the existing merge in services/env.ts, instead of letting the top-level spread replace it wholesale.

Testing

  • build passes
  • tests pass / added (3 new regression tests in loadConfig.test.ts, all 9 tests in the suite pass)
  • lint clean
  • CI: pending

🤖 Generated by the harbor agent loop. Reviewed by a human before merge.

Relates to #1922

Strip undefined-valued argv keys and yargs bookkeeping (_, $0), and
deep-merge an argv-supplied `service` instead of replacing it wholesale,
mirroring the sanitization already done by the env/git config loaders.
Previously an unset argv flag could silently clobber a resolved config
value with `undefined`, and a partial `service` override dropped the
rest of the resolved service config.

@gfargo-horizon-agent gfargo-horizon-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔎 Agent review (sonnet) — LGTM

REVIEW: LGTM
RESOLVES: full

The PR implements exactly the plan: strips yargs' _/$0, runs removeUndefined on argv before merging, and deep-merges service one level mirroring services/env.ts. All three regression tests pass, lint and typecheck are clean, the change is scoped to the two files the plan specified, and I verified downstream callers (commandExecutor.ts) get raw argv separately from the sanitized options, so stripping _/$0 from the merged config has no live-behavior impact.

@gfargo-horizon-agent gfargo-horizon-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔎 Agent review (sonnet) — LGTM

REVIEW: LGTM
RESOLVES: full

The diff replaces the shallow { ...config, ...argv } spread with a sanitized merge that strips yargs' _/$0, removes explicitly-undefined argv keys via the existing removeUndefined util, and deep-merges an argv-supplied service — exactly mirroring the pattern already used in services/env.ts. Three new targeted tests cover the undefined-clobber, bookkeeping-key-strip, and service-deep-merge cases, and all 9 tests in the file pass.

@gfargo-horizon-agent gfargo-horizon-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔎 Agent review (sonnet) — LGTM

REVIEW: LGTM
RESOLVES: full

The diff sanitizes argv before merging into resolved config exactly as the ticket specifies: strips yargs _/$0, filters undefined values via the existing removeUndefined helper, and deep-merges service one level, mirroring the identical precedent already established in services/env.ts. Scope is tight (only loadConfig.ts + its test file), and three new tests directly cover the three gaps called out in the work item.

@gfargo-horizon-agent gfargo-horizon-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔎 Agent re-review (sonnet, delta) — LGTM

REVIEW: LGTM
RESOLVES: full

The revise adds a shallow one-level deep-merge for the argv-supplied service object plus three targeted tests, matching the existing precedent in services/env.ts. No new bugs, scope creep, or missing coverage were introduced.

…1922-lib-2-fix-config-the-argv-spre

# Conflicts:
#	src/lib/config/utils/loadConfig.ts

@gfargo-horizon-agent gfargo-horizon-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔎 Agent re-review (sonnet, delta) — CONCERNS

REVIEW: CONCERNS
RESOLVES: partial

The added service deep-merge closes the shallow-replace gap (b), but it merges argvService without stripping nested undefined leaves, so a synthetic argv like { service: { provider: 'anthropic', model: undefined } } (the exact pattern the issue calls out for defaultRouter.ts) still clobbers a resolved service.model with undefined — reopening a narrower copy of bug (a) one level down. No test exercises this nested case.

1 concern — 1 inline on the diff

Comment thread src/lib/config/utils/loadConfig.ts Outdated
if (argvService && config.service) {
merged.service = {
...(config.service as object),
...(argvService as object),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

⚠️ Nested undefined in argv.service still clobbers resolved fields

cleanedArgv = removeUndefined(restArgv) only strips top-level undefined keys; it does not recurse into service. So if the argv object's service field itself contains an explicitly-undefined property (e.g. { provider: 'anthropic', model: undefined }, plausible from a hand-built synthetic argv like defaultRouter.ts produces), the deep-merge spreads that undefined over config.service, silently dropping a resolved model/authentication/endpoint value — the same clobbering bug the PR is fixing, just scoped to the nested object instead of the top level.

Suggested change
...(argvService as object),
merged.service = { ...(config.service as object), ...removeUndefined(argvService as Record<string, unknown>) } as Config['service']

…erge

The service deep-merge added in #2002 spread argv.service as-is, so a
synthetic argv like { service: { model: undefined } } (the shape
defaultRouter.ts hand-builds) still clobbered a resolved service.model
with undefined — reopening the original bug one level down.
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.

LIB-2: fix(config): the argv spread can clobber real config with undefined and shallow-replaces nested objects

0 participants