fix(config)!: stop loading the config file from the working directory - #1152
Conversation
The --config-file default fell back to the bare name "kosli" whenever $HOME/.kosli.yml was absent, which viper resolves against the current working directory. A checkout could therefore ship kosli.yml and set any flag for a command run with a real KOSLI_API_TOKEN: host or http-proxy sends the bearer token to a host of the repository's choosing, and the snapshot k8s kubeconfig flag runs a kubeconfig exec credential plugin, which --dry-run does not prevent. The default is now always the home config file, whether or not it exists. A config file in the working directory is loaded only when the user names it with --config-file or KOSLI_CONFIG_FILE. When no home directory can be resolved there is no default config file at all, rather than a bare name that reopens the working-directory search, and `kosli config` says so instead of writing a config file into the working directory. A warning names an ignored working-directory config so the change is not silent. It fires only for a file that sets org, api-token or host: a kosli.yml in a repository root is far more often a flow template, which was never loaded as CLI config. Refs kosli-dev/server#6778, kosli-dev/server#6779 BREAKING CHANGE: a kosli.{yaml,yml,json,toml} file in the current working directory is no longer loaded automatically. Pass --config-file, set KOSLI_CONFIG_FILE, or move the settings to $HOME/.kosli.yml with `kosli config`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @pbeckham's task in 6m 11s —— View job Review of
|
| Check | Result |
|---|---|
go build ./... |
clean |
go vet ./... |
clean |
go mod tidy + git diff --exit-code go.mod go.sum |
clean |
go test needs approval in this environment, so the suite was not run. Everything below is read from the sources.
Verified against viper 1.21
Both new commits claim to reproduce viper's search rule, so I checked the rule rather than the claim.
file.go:75-92 — searchInPath iterates SupportedExts and returns on the first hit, no parse attempted:
for _, ext := range SupportedExts {
if b, _ := exists(v.fs, filepath.Join(in, v.configName+"."+ext)); b {
return filepath.Join(in, v.configName+"."+ext)
}
}file.go:95-104 — exists is !stat.IsDir(), and swallows every non-IsNotExist error into false because the caller discards it:
stat, err := fs.Stat(path)
if err == nil {
return !stat.IsDir(), nil
}
if os.IsNotExist(err) { return false, nil }
return false, errroot.go:434-470 now mirrors all three branches exactly — continue on any stat error (broken symlink, ELOOP, EACCES), continue on a directory, return on everything the search would have stopped at. viper.go:304 confirms the order (json, toml, yaml, yml, properties, …) and encoding.go:166-181 confirms only json/toml/yaml/yml/dotenv/env have a decoder, so TestUndecodableWorkingDirConfigIsSilent rests on real behaviour and not on a coincidence. v.configType is never set, so the extensionless kosli fallback at file.go:85-89 stays unreachable and the name list is complete.
Also checked for test contamination: no tracked kosli.<ext> anywhere in the repo, and no t.Parallel() in cmd/kosli, so the t.Chdir calls are safe and no other suite starts emitting the warning.
Finding
kosli config cannot honour the remedy it is shown — root.go:422. config.go:68 rejects --config-file in its PreRunE, but the flag is inherited rather than shadowed, so shadowed is false and the full "pass --config-file kosli.yml" wording is printed. Following it gives cannot use --config-file with config command. Reachable on the likeliest path: the warning requires that no home config was loaded, which is exactly the state of a first-time kosli config run, and a checkout with a kosli.yml is the premise of the whole PR. Suggestion is inline, including the wording that stays true for snapshot k8s.
Not raising
- The 1 MB ceiling and the
IsRegularguard bothreturnsilently, so a genuinely >1 MB config or a FIFO-backed one is dropped without a word. Both match what viper would have stopped at, and the old CLI hung on the FIFO, so there is no behaviour to migrate — deliberate, and the comments say so. - Up to 12
os.Statcalls per command run in the working directory. Negligible next to a single HTTP request.
Good work
da5322af is the finding I raised, but the fix is better than what I suggested: splitting IsDir from IsRegular rather than folding them, with each branch's comment naming the viper line it mirrors, so the next reader can check the mirror instead of trusting it. 907e6717 retired a follow-up rather than adding one — stopping at the first existing name closed the extension-stripping mismatch as a side effect, which is the rare case of a fix shrinking the outstanding list.
The test suite has held up unusually well across nine rounds of review, and the reason is that each case pins why rather than what: TestNonRegularWorkingDirConfigIsNotParsed was quietly neutered by 907e6717 and the fix moved /dev/zero onto kosli.json with a comment explaining why the position is load-bearing, rather than leaving a green test guarding nothing. The 30-second FailNow harness turning a hang into a readable failure, and the stubHomeConfig helper making "no home config" the explicit precondition of every case, are both worth keeping as patterns.
· branch fix-no-implicit-cwd-config
Review follow-ups on the working-directory config removal. Derive the candidate names from viper.SupportedExts rather than listing four by hand. viper searched a "kosli" config name in every extension it supports and loaded the first match, so kosli.env and kosli.dotenv were also working redirects and went unwarned. The six formats viper lists but has no decoder for made every command fail with "failed to parse config file" before this change, so they stay silent: nothing could have depended on them. Warn on any parseable file except a flow template, instead of on a chosen set of keys. Any key set narrow enough to write down lets some real config break in silence, which is the one thing the warning exists to prevent: a file holding only http-proxy, kubeconfig or flow was ignored and silent. A top-level trail or artifacts key marks a flow template, which is passed with --template-file and was never loaded as CLI config. Cap the file at 1 MB before parsing. It is repository-controlled and read on every command run in that directory. Warn after the flag binding so KOSLI_QUIET suppresses the message exactly as --quiet does, and pin that the home config file is still loaded, which no test covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lates by shape Two more review follow-ups. Warn only while the home config file is absent. The old default fell back to the working directory only in that state, so a user who has $HOME/.kosli.yml never loaded the working-directory file. For them the message was both wrong and harmful: --config-file replaces the home config rather than adding to it, so following the advice would have dropped their org and api token. The gate is evaluated before bindFlags, which can overwrite global.ConfigFile from a config file of its own. Identify a flow template by shape rather than by key name. trail and artifacts are CLI flags as well as template keys, so a config file holding trail: my-trail was ignored and silent, which is what the file-level check was chosen to avoid. A template's trail is a mapping and its artifacts a sequence, where the flags take a string, so the two are told apart without guessing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…read The --config-file usage string and the `kosli config` description are what the generated CLI reference is built from, and neither mentioned the removed behaviour. A reader of `kosli list flows --help` had only the (default ...) suffix to tell them a kosli.yml beside them is no longer picked up. The `kosli config` precedence list also named $HOME/.kosli, which has never been the filename. Corrected here rather than left contradicting the path added on the line above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerated from kosli-dev/cli 3d2c424c: the --config-file description now says config is never read implicitly from the current directory, and the kosli config precedence list names $HOME/.kosli.yml rather than $HOME/.kosli. Refs kosli-dev/cli#1152
…t behaviour Two more review follow-ups, both on the warning rather than the fix. Skip anything that is not a regular file. os.Stat follows symlinks, so a checkout could ship kosli.yml -> /dev/zero, which reports IsDir false and Size 0 while an unbounded read waits behind it, and the size ceiling never applied. Only a regular file's Size says how much there is to read. IsRegular also covers the directory case it replaces. Ask viper what it loaded instead of stat'ing one filename. The home config is read by config name, so ~/.kosli.json is a home config too, and its owner was told their working-directory file is no longer loaded and to pass --config-file, which would have replaced the home config viper had just read for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… command snapshot k8s declares a local --config-file for its namespace selectors, and cobra's flag merge keeps the local one, so cmd.Flags().Lookup answered about the wrong flag on that command. Two consequences, one from this PR and one older. The warning about an ignored working-directory config was suppressed by passing an unrelated flag, on the command #6779 was reported against. And KOSLI_CONFIG_FILE was ignored whenever snapshot k8s --config-file was given, because the guard saw the k8s flag as changed. Looking the flag up on the root is unambiguous and behaves identically for every other command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The warning is emitted in PersistentPreRunE, which does not stop the run, and --api-token DRY_RUN suppresses only the Kosli request. The command therefore continued into runMultiEnv with --kubeconfig defaulted to $HOME/.kube/config: green in CI because the connection failed and the error was discarded, but on a machine with a current context it listed pods in the default namespace of whatever cluster that was. Naming a kubeconfig that cannot exist stops the run before any cluster is reached and turns the discarded error into an assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On snapshot k8s none of --config-file, -c or KOSLI_CONFIG_FILE names the Kosli config file: the command declares its own --config-file, cobra's merge drops the root's along with the shorthand, and bindFlags writes KOSLI_CONFIG_FILE into the k8s flag. Offering all three there sent a user into runMultiEnv with the file they were trying to keep, on the command #6779 was reported against. The warning now names only the remedy that works when the flag is shadowed. The shadow is what is tested for, rather than the root flag, so a caller reaching here before cobra's flag merge gets the message that is true of every other command instead of one claiming a flag the command does not declare. Also on the non-regular-file test: FailNow rather than Fail, since the goroutine is still inside the unbounded read and continuing would restore the working directory from under a live command, and skip on Windows, where /dev/zero and os.Symlink do not apply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
viper's searchInPath returned the first existing kosli.* name in SupportedExts order and stopped, whatever that file contained. The warning loop instead skipped past a template, an empty file or an unreadable one and landed on a later name, so it could say a file "is no longer loaded automatically" when that file was never loaded: with a kosli.json template beside a real kosli.yml, viper read the template. The remedy compounded it. --config-file strips the extension and searches the config name again, so following the advice loaded the template the warning had classified as noise. Every check after the existence test now decides whether to warn about this one file rather than whether to move on, which is viper's rule exactly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
907e671 folded the directory case into the return branch added for /dev/zero, and the two want opposite answers. viper's existence check is !stat.IsDir(), so a directory named kosli.json was not the file it loaded and the search carried on; a character device was, so the loop must stop. Merging them meant a directory kosli.json beside a real kosli.yml dropped the yml in silence, which is the outcome the warning exists to prevent. The same commit also made the non-regular-file test vacuous: the loop now stops at the first existing name, so kosli.yml -> /dev/zero was never reached and the test passed with the IsRegular guard deleted. The symlink moves to kosli.json, the first name in viper's order, putting the unbounded read back on the path the loop takes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes kosli-dev/server#6778 and kosli-dev/server#6779.
The
--config-filedefault fell back to the bare namekosliwhenever$HOME/.kosli.ymlwas absent, and viper resolves that against the current working directory. A checkout could therefore shipkosli.ymland set any Kosli flag for a command run with a real API token. The default is now always the home config file, and a working-directory config is read only when named.Why
Two confirmed vulnerabilities, both reachable by a developer or CI job running an ordinary Kosli command in an untrusted checkout with
KOSLI_API_TOKENset. A repository-controlledhostorhttp-proxysendsAuthorization: Bearer <real token>to a host of the repository's choosing. A repository-controlledkubeconfigonsnapshot k8sruns a kubeconfigexeccredential plugin, which is arbitrary command execution, and--dry-rundoes not prevent it. Exposure is widest where tokens live: a CI runner has no$HOME/.kosli.yml, the state that triggered the fallback.What changed
getConfigFileFlagDefaultno longer degrades to a relative name, so viper's search path is always the home directory.initializeskips the config read when no default path resolves, andkosli configfails clearly rather than writing into the working directory. The--config-fileandkosli confighelp strings now say config is never read implicitly from the current directory, since the published reference is generated from them.Behaviour is unchanged for
--config-file,KOSLI_CONFIG_FILE,$HOME/.kosli.ymlandKOSLI_*environment variables.So the removal does not break a pipeline silently, an ignored working-directory config produces a warning naming the file and the fix. When to warn is the fiddly half: only while no home config was loaded, since nobody else lost behaviour and
--config-filewould replace a home config rather than restore one; only for a decodable regular file under 1 MB, since it is repository-controlled and read on every command run; and never for a flow template, told apart bytrailbeing a mapping orartifactsa sequence, because both are CLI flags as well as template keys.Verification
New
WorkingDirConfigTestSuiteincmd/kosli/configWorkingDir_test.go, 14 cases. They pin that none of the six decodable extensions can sethostfrom the working directory, the warning text across nine file shapes, the home-config load path in both.ymland.jsonform, and that akosli.yml -> /dev/zerosymlink is skipped rather than read unbounded.Both reported scenarios were reproduced against a build of
a5ecf036and re-run against this branch: the bearer token now stops at the default host, and the kubeconfig credential plugin does not execute. The Test workflow gates this PR withmake test_integration_fulland golangci-lint.Follow-up
Docs are on
6778-no-implicit-cwd-configin kosli-dev/docs, to merge with the release.