From 60bfdff7a667aba18a494c14503f752a5c70a604 Mon Sep 17 00:00:00 2001 From: Shanto Islam Date: Wed, 27 May 2026 11:43:52 +0600 Subject: [PATCH 1/6] fix(security): harden checksum verification, pin CDN with SRI, and cleanup dead code Security fixes: - npm postinstall: checksum verification now hard-fails instead of warning, preventing tampered binaries from installing silently - website: pin Lucide to v1.16.0 with SHA-384 SRI hash to prevent CDN compromise Extensibility fixes: - config loader: remove hard-rejection of non-'openai' provider types, allowing custom providers via RegisterFactory() - cli: wire Config.Validate() into initConfig() to catch invalid configs early Cleanup: - remove duplicate resume validation from CLI (generator already validates) - remove dead code: StreamChunk struct, ErrKindQuota, ErrKindContentFilter, Duration.MarshalYAML(), global Register/RegisterFactory functions - remove unsupported 'logging:' section from example config - add --verbose/--quiet mutual exclusion via cobra --- examples/kothaset.yaml | 4 ---- internal/cli/generate.go | 24 ------------------------ internal/cli/root.go | 6 ++++++ internal/config/config.go | 5 +---- internal/config/loader.go | 3 --- internal/provider/errors.go | 4 ++-- internal/provider/provider.go | 16 ---------------- internal/provider/registry.go | 10 ---------- internal/schema/registry.go | 5 ----- internal/schema/registry_test.go | 12 +++--------- npm/scripts/postinstall.js | 3 ++- website/index.html | 4 +++- 12 files changed, 17 insertions(+), 79 deletions(-) diff --git a/examples/kothaset.yaml b/examples/kothaset.yaml index 66facd3..aa03cb4 100644 --- a/examples/kothaset.yaml +++ b/examples/kothaset.yaml @@ -21,7 +21,3 @@ instructions: - Be creative and diverse in topics and approaches - Vary the style and complexity of responses - Use clear and concise language - -logging: - level: info - format: text diff --git a/internal/cli/generate.go b/internal/cli/generate.go index 9bf7215..727ce27 100644 --- a/internal/cli/generate.go +++ b/internal/cli/generate.go @@ -14,7 +14,6 @@ import ( "github.com/schollz/progressbar/v3" "github.com/spf13/cobra" - "github.com/shantoislamdev/kothaset/internal/fsutil" "github.com/shantoislamdev/kothaset/internal/generator" log "github.com/shantoislamdev/kothaset/internal/log" "github.com/shantoislamdev/kothaset/internal/output" @@ -134,29 +133,6 @@ func runGenerate(cmd *cobra.Command, _ []string) error { if genModel == "" { genModel = cp.Config.Model } - - // Guardrails against accidentally resuming into a different run target. - if cmd.Flags().Changed("schema") && cp.Config.Schema != "" && genSchema != cp.Config.Schema { - return fmt.Errorf("resume schema mismatch: checkpoint=%s current=%s", cp.Config.Schema, genSchema) - } - if cmd.Flags().Changed("provider") && cp.Config.Provider != "" && genProvider != cp.Config.Provider { - return fmt.Errorf("resume provider mismatch: checkpoint=%s current=%s", cp.Config.Provider, genProvider) - } - if cmd.Flags().Changed("model") && cp.Config.Model != "" && genModel != cp.Config.Model { - return fmt.Errorf("resume model mismatch: checkpoint=%s current=%s", cp.Config.Model, genModel) - } - if cmd.Flags().Changed("input") && cp.Config.InputFile != "" && genInputFile != cp.Config.InputFile { - return fmt.Errorf("resume input mismatch: checkpoint=%s current=%s", cp.Config.InputFile, genInputFile) - } - if cmd.Flags().Changed("output") && cp.Config.OutputPath != "" { - sameOutput, err := fsutil.PathsEqual(genOutput, cp.Config.OutputPath) - if err != nil { - return fmt.Errorf("failed to compare output path with checkpoint output path: %w", err) - } - if !sameOutput { - return fmt.Errorf("resume output mismatch: checkpoint=%s current=%s", cp.Config.OutputPath, genOutput) - } - } } if genInputFile == "" { diff --git a/internal/cli/root.go b/internal/cli/root.go index 6e408d9..c7476ae 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -79,6 +79,8 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable debug logging") rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "suppress non-error output") + rootCmd.MarkFlagsMutuallyExclusive("verbose", "quiet") + // Register subcommands rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(initCmd) @@ -104,6 +106,10 @@ func initConfig() error { return fmt.Errorf("failed to load .secrets.yaml: %w", err) } + if err := cfg.Validate(); err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + return nil } diff --git a/internal/config/config.go b/internal/config/config.go index 96493b1..e3d9cfe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -130,10 +130,7 @@ func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { } } -// MarshalYAML implements yaml.Marshaler -func (d Duration) MarshalYAML() (interface{}, error) { - return d.String(), nil -} + // DefaultConfig returns a configuration with sensible defaults func DefaultConfig() *Config { diff --git a/internal/config/loader.go b/internal/config/loader.go index 2241714..a3ff0f3 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -135,9 +135,6 @@ func LoadSecretsConfig(secretsPath string) (*SecretsConfig, error) { if p.Type == "" { return nil, fmt.Errorf("provider %s type is required", p.Name) } - if p.Type != "openai" { - return nil, fmt.Errorf("unsupported provider type: %s", p.Type) - } } return secrets, nil diff --git a/internal/provider/errors.go b/internal/provider/errors.go index 88315d7..c7acea4 100644 --- a/internal/provider/errors.go +++ b/internal/provider/errors.go @@ -12,11 +12,11 @@ const ( ErrKindValidation ErrorKind = "validation" // Invalid request parameters ErrKindAuth ErrorKind = "auth" // Authentication failure ErrKindRateLimit ErrorKind = "rate_limit" // Rate limit exceeded - ErrKindQuota ErrorKind = "quota" // Quota exceeded + ErrKindNetwork ErrorKind = "network" // Network connectivity issue ErrKindTimeout ErrorKind = "timeout" // Request timeout ErrKindServer ErrorKind = "server" // Provider server error - ErrKindContentFilter ErrorKind = "content_filter" // Content filtered + ErrKindContextLength ErrorKind = "context_length" // Context too long ErrKindUnknown ErrorKind = "unknown" // Unknown error ) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 1e2b079..4c149c5 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -97,20 +97,4 @@ type TokenUsage struct { TotalTokens int `json:"total_tokens"` } -// StreamChunk represents a piece of a streaming response -type StreamChunk struct { - // Content is the text delta - Content string `json:"content"` - - // Done indicates the stream is complete - Done bool `json:"done"` - - // FinishReason when Done is true - FinishReason string `json:"finish_reason,omitempty"` - // Usage when Done is true - Usage *TokenUsage `json:"usage,omitempty"` - - // Error if something went wrong - Error error `json:"error,omitempty"` -} diff --git a/internal/provider/registry.go b/internal/provider/registry.go index 5cd84eb..1b8af1a 100644 --- a/internal/provider/registry.go +++ b/internal/provider/registry.go @@ -141,16 +141,6 @@ func (r *Registry) Close() error { // Global registry functions -// RegisterFactory registers a factory in the global registry -func RegisterFactory(providerType string, factory Factory) { - globalRegistry.RegisterFactory(providerType, factory) -} - -// Register adds a provider to the global registry -func Register(name string, provider Provider) error { - return globalRegistry.Register(name, provider) -} - // Get retrieves a provider from the global registry func Get(name string) (Provider, error) { return globalRegistry.Get(name) diff --git a/internal/schema/registry.go b/internal/schema/registry.go index 75e6097..7278a25 100644 --- a/internal/schema/registry.go +++ b/internal/schema/registry.go @@ -78,11 +78,6 @@ func (r *Registry) List() []string { // Global registry functions -// Register adds a schema to the global registry -func Register(schema Schema) error { - return globalRegistry.Register(schema) -} - // Get retrieves a schema from the global registry func Get(name string) (Schema, error) { return globalRegistry.Get(name) diff --git a/internal/schema/registry_test.go b/internal/schema/registry_test.go index 7844afb..761f76e 100644 --- a/internal/schema/registry_test.go +++ b/internal/schema/registry_test.go @@ -138,16 +138,10 @@ func TestGlobalRegistry(t *testing.T) { mGlobal := &mockSchema{name: "global_mock"} - // Test Register - err := Register(mGlobal) + // Register via the registry directly + err := globalRegistry.Register(mGlobal) if err != nil { - t.Errorf("Global Register failed: %v", err) - } - - // Duplicate registration should fail - err = Register(mGlobal) - if err == nil { - t.Error("Global Register should fail on duplicate") + t.Errorf("Register failed: %v", err) } // Test Get diff --git a/npm/scripts/postinstall.js b/npm/scripts/postinstall.js index d5fba3d..f483156 100644 --- a/npm/scripts/postinstall.js +++ b/npm/scripts/postinstall.js @@ -205,7 +205,8 @@ async function main() { console.warn(`Warning: no checksum found for ${releaseName}`); } } catch (checksumErr) { - console.warn(`Warning: checksum verification skipped: ${checksumErr.message}`); + console.error(`Checksum verification failed: ${checksumErr.message}`); + throw checksumErr; } // Extract diff --git a/website/index.html b/website/index.html index 710e5b1..320eb99 100644 --- a/website/index.html +++ b/website/index.html @@ -17,7 +17,9 @@ - + From e73907999e77003b845bd1d297c298702a5dc22c Mon Sep 17 00:00:00 2001 From: Shanto Islam Date: Wed, 27 May 2026 11:47:18 +0600 Subject: [PATCH 2/6] fix(lint): apply gofmt formatting --- internal/config/config.go | 2 -- internal/provider/errors.go | 12 ++++++------ internal/provider/provider.go | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e3d9cfe..f5c81a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -130,8 +130,6 @@ func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { } } - - // DefaultConfig returns a configuration with sensible defaults func DefaultConfig() *Config { return &Config{ diff --git a/internal/provider/errors.go b/internal/provider/errors.go index c7acea4..1feda04 100644 --- a/internal/provider/errors.go +++ b/internal/provider/errors.go @@ -9,13 +9,13 @@ import ( type ErrorKind string const ( - ErrKindValidation ErrorKind = "validation" // Invalid request parameters - ErrKindAuth ErrorKind = "auth" // Authentication failure - ErrKindRateLimit ErrorKind = "rate_limit" // Rate limit exceeded + ErrKindValidation ErrorKind = "validation" // Invalid request parameters + ErrKindAuth ErrorKind = "auth" // Authentication failure + ErrKindRateLimit ErrorKind = "rate_limit" // Rate limit exceeded - ErrKindNetwork ErrorKind = "network" // Network connectivity issue - ErrKindTimeout ErrorKind = "timeout" // Request timeout - ErrKindServer ErrorKind = "server" // Provider server error + ErrKindNetwork ErrorKind = "network" // Network connectivity issue + ErrKindTimeout ErrorKind = "timeout" // Request timeout + ErrKindServer ErrorKind = "server" // Provider server error ErrKindContextLength ErrorKind = "context_length" // Context too long ErrKindUnknown ErrorKind = "unknown" // Unknown error diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4c149c5..964bf2b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -96,5 +96,3 @@ type TokenUsage struct { CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } - - From 64199a6860fc01d3f45f1591255b8fdd86216660 Mon Sep 17 00:00:00 2001 From: Shanto Islam Date: Wed, 27 May 2026 11:50:31 +0600 Subject: [PATCH 3/6] fix: move config validation to generate command only Validate() in initConfig() caused commands like 'validate config' and 'schema list' to fail when an invalid kothaset.yaml existed in the cwd. Move validation into runGenerate() where a usable runtime config is actually required. --- internal/cli/generate.go | 4 ++++ internal/cli/root.go | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cli/generate.go b/internal/cli/generate.go index 727ce27..610043d 100644 --- a/internal/cli/generate.go +++ b/internal/cli/generate.go @@ -153,6 +153,10 @@ func runGenerate(cmd *cobra.Command, _ []string) error { return fmt.Errorf("configuration not loaded") } + if err := cfg.Validate(); err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + // Handle seed (optional) // Supports: empty (no seed), "random" (different random per request), or a specific number (fixed seed) var seedPtr *int64 diff --git a/internal/cli/root.go b/internal/cli/root.go index c7476ae..9bddaac 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -106,10 +106,6 @@ func initConfig() error { return fmt.Errorf("failed to load .secrets.yaml: %w", err) } - if err := cfg.Validate(); err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - return nil } From 37ea780a47a51f1b728f17f57fe4e9709225a664 Mon Sep 17 00:00:00 2001 From: Shanto Islam Date: Wed, 27 May 2026 11:51:51 +0600 Subject: [PATCH 4/6] fix(npm): require checksum entry before extracting A missing entry in checksums.txt (from omission or tampering) now throws instead of warning, preventing unverified binaries from installing silently. --- npm/scripts/postinstall.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/npm/scripts/postinstall.js b/npm/scripts/postinstall.js index f483156..0b1c319 100644 --- a/npm/scripts/postinstall.js +++ b/npm/scripts/postinstall.js @@ -198,12 +198,11 @@ async function main() { const checksums = await downloadChecksums(VERSION); const releaseName = path.basename(new URL(url).pathname); const expectedHash = checksums[releaseName]; - if (expectedHash) { - await verifyChecksum(archivePath, expectedHash); - console.log('Checksum verified.'); - } else { - console.warn(`Warning: no checksum found for ${releaseName}`); + if (!expectedHash) { + throw new Error(`no checksum entry for ${releaseName} in checksums.txt`); } + await verifyChecksum(archivePath, expectedHash); + console.log('Checksum verified.'); } catch (checksumErr) { console.error(`Checksum verification failed: ${checksumErr.message}`); throw checksumErr; From c30b40eb4f6ccb757eb057dc67fe3589c8a77427 Mon Sep 17 00:00:00 2001 From: Shanto Islam Date: Wed, 27 May 2026 12:04:49 +0600 Subject: [PATCH 5/6] fix(lint): resolve all golangci-lint issues - Apply gofmt to all Go files - Use fmt.Fprintf instead of WriteString(fmt.Sprintf) in classification.go and preference.go --- internal/schema/classification.go | 4 ++-- internal/schema/preference.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/schema/classification.go b/internal/schema/classification.go index 68daddb..16f6954 100644 --- a/internal/schema/classification.go +++ b/internal/schema/classification.go @@ -69,7 +69,7 @@ func (s *ClassificationSchema) GeneratePrompt(ctx context.Context, opts PromptOp } if opts.Topic != "" { - sb.WriteString(fmt.Sprintf("Category/Domain: %s\n", opts.Topic)) + fmt.Fprintf(&sb, "Category/Domain: %s\n", opts.Topic) } // Get labels from variables if provided @@ -83,7 +83,7 @@ func (s *ClassificationSchema) GeneratePrompt(ctx context.Context, opts PromptOp sb.WriteString("\n") if len(labels) > 0 { - sb.WriteString(fmt.Sprintf("Available labels: %s\n\n", strings.Join(labels, ", "))) + fmt.Fprintf(&sb, "Available labels: %s\n\n", strings.Join(labels, ", ")) sb.WriteString(`Generate a text sample and assign the most appropriate label: { diff --git a/internal/schema/preference.go b/internal/schema/preference.go index 4e363aa..b707f7e 100644 --- a/internal/schema/preference.go +++ b/internal/schema/preference.go @@ -63,10 +63,10 @@ func (s *PreferenceSchema) GeneratePrompt(ctx context.Context, opts PromptOption } if opts.Topic != "" { - sb.WriteString(fmt.Sprintf("Topic: %s\n", opts.Topic)) + fmt.Fprintf(&sb, "Topic: %s\n", opts.Topic) } if opts.Category != "" { - sb.WriteString(fmt.Sprintf("Category: %s\n", opts.Category)) + fmt.Fprintf(&sb, "Category: %s\n", opts.Category) } sb.WriteString("\n") From 510ecf5f68a4a33f462f6bf5efa542c83665b0f1 Mon Sep 17 00:00:00 2001 From: Shanto Islam Date: Wed, 27 May 2026 12:08:58 +0600 Subject: [PATCH 6/6] fix: remove premature config validation from generate command cfg.Validate() checked raw kothaset.yaml values before CLI overrides were merged, causing commands like 'generate --schema chat' to fail when the config contained a different schema. All fields are already validated individually after resolution (schema.Get, GetProvider, etc). --- internal/cli/generate.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/cli/generate.go b/internal/cli/generate.go index 610043d..727ce27 100644 --- a/internal/cli/generate.go +++ b/internal/cli/generate.go @@ -153,10 +153,6 @@ func runGenerate(cmd *cobra.Command, _ []string) error { return fmt.Errorf("configuration not loaded") } - if err := cfg.Validate(); err != nil { - return fmt.Errorf("invalid configuration: %w", err) - } - // Handle seed (optional) // Supports: empty (no seed), "random" (different random per request), or a specific number (fixed seed) var seedPtr *int64