Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21ae2159f7 | |||
| b9425c9ae2 | |||
| fbe7b61c79 |
3
.codebase-memory/.gitattributes
vendored
3
.codebase-memory/.gitattributes
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Auto-generated by codebase-memory-mcp
|
|
||||||
# Prevent merge conflicts on compressed artifact
|
|
||||||
graph.db.zst merge=ours binary
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": 2,
|
|
||||||
"commit": "88e86de03ebd87038b0c98b164865f2c3b82a94c",
|
|
||||||
"indexed_at": "2026-09-12T18:30:58Z",
|
|
||||||
"project": "home-eric-Workspace-ConfigKit",
|
|
||||||
"nodes": 1641,
|
|
||||||
"edges": 7961,
|
|
||||||
"original_size": 8126464,
|
|
||||||
"compressed_size": 1350795,
|
|
||||||
"compression_level": 3
|
|
||||||
}
|
|
||||||
Binary file not shown.
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,6 +25,9 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Local codebase-memory tooling artifacts
|
||||||
|
/.codebase-memory/
|
||||||
|
|
||||||
# ---> Linux
|
# ---> Linux
|
||||||
*~
|
*~
|
||||||
|
|
||||||
@@ -81,4 +84,3 @@ Temporary Items
|
|||||||
.history/
|
.history/
|
||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
|
||||||
|
|||||||
@@ -293,10 +293,13 @@ operation once before starting concurrent work. The selected path may itself be
|
|||||||
symlink; ConfigKit resolves it to an existing canonical directory before examining
|
symlink; ConfigKit resolves it to an existing canonical directory before examining
|
||||||
its immediate entries.
|
its immediate entries.
|
||||||
|
|
||||||
The bootstrap enumerates at most 1,024 entries and sorts them by filename. Every
|
The bootstrap enumerates at most 1,024 entries, including dot-prefixed entries.
|
||||||
entry must have an ASCII environment-variable name and be a regular, nonsymlink
|
Entries whose names begin with `.` (files, directories, or symlinks, such as `.git`
|
||||||
file. Metadata for all entries is validated before file content is read or process
|
and `.gitignore`) are ignored before name or metadata validation, are never opened,
|
||||||
state is changed. Existing environment variables win, including variables whose
|
and are omitted from the report. Remaining entries are sorted by filename; each
|
||||||
|
must have an ASCII environment-variable name and be a regular, nonsymlink
|
||||||
|
file. Metadata for all remaining entries is validated before file content is read
|
||||||
|
or process state is changed. Existing environment variables win, including variables whose
|
||||||
value is empty; their files are not opened. Each eligible file is limited to 1 MiB,
|
value is empty; their files are not opened. Each eligible file is limited to 1 MiB,
|
||||||
and eligible content is limited to 8 MiB in total. ConfigKit removes exactly one
|
and eligible content is limited to 8 MiB in total. ConfigKit removes exactly one
|
||||||
terminal LF or CRLF, preserves all other whitespace, accepts an empty value, and
|
terminal LF or CRLF, preserves all other whitespace, accepts an empty value, and
|
||||||
|
|||||||
43
docs/releases/v0.3.1.md
Normal file
43
docs/releases/v0.3.1.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ConfigKit v0.3.1
|
||||||
|
|
||||||
|
This release fixes secrets-directory bootstrap when the directory contains Git
|
||||||
|
metadata or other dot-prefixed entries.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`LoadSecretsDir` now ignores dot-prefixed files, directories, and symlinks,
|
||||||
|
including `.git`, `.gitignore`, and `.DS_Store`. These entries no longer prevent
|
||||||
|
otherwise valid secrets from loading and are omitted from the bootstrap report.
|
||||||
|
See the [bootstrap contract](../config.md#explicit-secrets-directory-bootstrap).
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This is a backward-compatible bug fix for previously rejected directory contents.
|
||||||
|
There are no Go API signature changes or migrations for previously accepted
|
||||||
|
inputs. Validation of non-hidden entries, existing-environment precedence, and
|
||||||
|
the existing enumeration and content limits remain unchanged. Hidden entries
|
||||||
|
still count toward the directory enumeration limit.
|
||||||
|
|
||||||
|
The minimum Go version remains 1.25. Linux remains release-validated and macOS
|
||||||
|
remains best-effort. The explicitly pinned YAML dependency remains
|
||||||
|
`go.yaml.in/yaml/v4 v4.0.0-rc.6`; reassessment found no newer published version.
|
||||||
|
Retaining this prerelease pin avoids unrelated parser changes in this patch.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
Update the module dependency and rebuild the consuming application:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/ConfigKit@v0.3.1
|
||||||
|
```
|
||||||
|
|
||||||
|
No configuration migration is required. A secrets directory can retain its
|
||||||
|
dot-prefixed metadata without treating those entries as secret candidates.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Filter hidden entries before filename and metadata validation and reporting.
|
||||||
|
- Cover hidden-only and mixed directories, including Git metadata and a dangling
|
||||||
|
hidden symlink, with regression tests.
|
||||||
|
- Update public API comments and the canonical bootstrap contract.
|
||||||
|
- Stop tracking local codebase-memory tooling artifacts and ignore their directory.
|
||||||
@@ -130,6 +130,7 @@ type stagedSecret struct {
|
|||||||
// LoadSecretsDir explicitly loads immediate regular files from directory into
|
// LoadSecretsDir explicitly loads immediate regular files from directory into
|
||||||
// previously unset process environment variables. Applications should call it
|
// previously unset process environment variables. Applications should call it
|
||||||
// once during startup, before concurrent work; Load never calls it.
|
// once during startup, before concurrent work; Load never calls it.
|
||||||
|
// Dot-prefixed entries are ignored and omitted from the report.
|
||||||
func LoadSecretsDir(directory string) (*SecretsReport, error) {
|
func LoadSecretsDir(directory string) (*SecretsReport, error) {
|
||||||
return loadSecretsDir(directory, secretsEnvironment{lookup: os.LookupEnv, set: os.Setenv}, secretsAccess{
|
return loadSecretsDir(directory, secretsEnvironment{lookup: os.LookupEnv, set: os.Setenv}, secretsAccess{
|
||||||
openDirectory: os.Open,
|
openDirectory: os.Open,
|
||||||
@@ -168,13 +169,17 @@ func loadSecretsDir(directory string, environment secretsEnvironment, access sec
|
|||||||
if closeErr != nil {
|
if closeErr != nil {
|
||||||
return report, secretsFailure(CodeSecretsRead, "secrets directory could not be closed: "+strconv.Quote(canonical))
|
return report, secretsFailure(CodeSecretsRead, "secrets directory could not be closed: "+strconv.Quote(canonical))
|
||||||
}
|
}
|
||||||
|
entryLimitExceeded := len(entries) > secretsEntryLimit
|
||||||
|
entries = slices.DeleteFunc(entries, func(entry fs.DirEntry) bool {
|
||||||
|
return strings.HasPrefix(entry.Name(), ".")
|
||||||
|
})
|
||||||
slices.SortFunc(entries, func(left, right fs.DirEntry) int { return strings.Compare(left.Name(), right.Name()) })
|
slices.SortFunc(entries, func(left, right fs.DirEntry) int { return strings.Compare(left.Name(), right.Name()) })
|
||||||
report.entries = make([]SecretFileResult, len(entries))
|
report.entries = make([]SecretFileResult, len(entries))
|
||||||
for index, entry := range entries {
|
for index, entry := range entries {
|
||||||
name := entry.Name()
|
name := entry.Name()
|
||||||
report.entries[index] = SecretFileResult{Name: name, Path: filepath.Join(canonical, name), Status: SecretFileNotApplied}
|
report.entries[index] = SecretFileResult{Name: name, Path: filepath.Join(canonical, name), Status: SecretFileNotApplied}
|
||||||
}
|
}
|
||||||
if len(entries) > secretsEntryLimit {
|
if entryLimitExceeded {
|
||||||
return report, secretsFailure(CodeSecretsLimitExceeded, "secrets directory exceeds the 1024-entry limit: "+strconv.Quote(canonical))
|
return report, secretsFailure(CodeSecretsLimitExceeded, "secrets directory exceeds the 1024-entry limit: "+strconv.Quote(canonical))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,41 @@ func TestLoadSecretsDirLoadsCanonicalSortedValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadSecretsDirIgnoresDotEntries(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
if err := os.Mkdir(filepath.Join(directory, ".git"), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeSecretTestFile(t, directory, ".gitignore", "CANARY\x00")
|
||||||
|
writeSecretTestFile(t, directory, ".DS_Store", "CANARY\x00")
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := os.Symlink(filepath.Join(directory, "missing"), filepath.Join(directory, ".hidden-link")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
access := testSecretsAccess()
|
||||||
|
access.lstat = func(path string) (fs.FileInfo, error) {
|
||||||
|
if strings.HasPrefix(filepath.Base(path), ".") {
|
||||||
|
t.Fatalf("inspected ignored entry %q", path)
|
||||||
|
}
|
||||||
|
return os.Lstat(path)
|
||||||
|
}
|
||||||
|
environment := map[string]string{}
|
||||||
|
report, err := loadSecretsDir(directory, mapSecretsEnvironment(environment), access)
|
||||||
|
if err != nil || len(report.Entries()) != 0 || len(environment) != 0 {
|
||||||
|
t.Fatalf("hidden-only directory: error = %v, report = %#v, environment = %#v", err, report.Entries(), environment)
|
||||||
|
}
|
||||||
|
writeSecretTestFile(t, directory, "SECRET", "value\n")
|
||||||
|
report, err = loadSecretsDir(directory, mapSecretsEnvironment(environment), access)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []SecretFileResult{{Name: "SECRET", Path: filepath.Join(canonicalTestPath(t, directory), "SECRET"), Status: SecretFileLoaded}}
|
||||||
|
if !reflect.DeepEqual(report.Entries(), want) || !reflect.DeepEqual(environment, map[string]string{"SECRET": "value"}) {
|
||||||
|
t.Fatalf("mixed directory: report = %#v, environment = %#v", report.Entries(), environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadSecretsDirPreservesExistingWithoutReading(t *testing.T) {
|
func TestLoadSecretsDirPreservesExistingWithoutReading(t *testing.T) {
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
writeSecretTestFile(t, directory, "EMPTY_WINNER", strings.Repeat("CANARY", int(secretFileByteLimit)))
|
writeSecretTestFile(t, directory, "EMPTY_WINNER", strings.Repeat("CANARY", int(secretFileByteLimit)))
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func (e *SecretsError) Diagnostics() []Diagnostic {
|
|||||||
// LoadSecretsDir explicitly loads immediate regular files from directory into
|
// LoadSecretsDir explicitly loads immediate regular files from directory into
|
||||||
// previously unset process environment variables. Applications should call it
|
// previously unset process environment variables. Applications should call it
|
||||||
// once during startup, before concurrent work; Load never calls it.
|
// once during startup, before concurrent work; Load never calls it.
|
||||||
|
// Dot-prefixed entries are ignored and omitted from the report.
|
||||||
func LoadSecretsDir(directory string) (*SecretsReport, error) {
|
func LoadSecretsDir(directory string) (*SecretsReport, error) {
|
||||||
report, err := engine.LoadSecretsDir(directory)
|
report, err := engine.LoadSecretsDir(directory)
|
||||||
result := &SecretsReport{value: report}
|
result := &SecretsReport{value: report}
|
||||||
|
|||||||
Reference in New Issue
Block a user