{"record":{"id":"44d749268566ef1f","repo":"Billionmail/BillionMail","slug":"error-saving-default-prompt-config-file-v","errorCode":null,"errorMessage":"error saving default prompt config file: %v","messagePattern":"error saving default prompt config file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/askai/project.go","lineNumber":795,"sourceCode":"\tfilename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/prompt_config.json\", Domain)\n\tif !public.FileExists(filename) {\n\t\t// If the prompt config file does not exist, return a default prompt config\n\t\t// This allows the system to handle cases where the prompt configuration has not been set up\n\t\t// and avoids errors when trying to read a non-existent file.\n\t\t// It also allows the user to create a new prompt configuration without needing to handle file\n\t\t// not found errors.\n\t\tdefaultPromptConfig := PromptConfig{\n\t\t\tPrompt: \"This is a default prompt. Please customize it.\",\n\t\t}\n\t\tdefaultPromptConfig.UpdateTime = public.GetNowTime()\n\t\tdefaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, \"\", \"  \")\n\n\t\tif err != nil {\n\t\t\treturn PromptConfig{}, fmt.Errorf(\"error marshalling default prompt config: %v\", err)\n\t\t}\n\t\terr = os.WriteFile(filename, defaultPromptJson, 0644)\n\t\tif err != nil {\n\t\t\treturn PromptConfig{}, fmt.Errorf(\"error saving default prompt config file: %v\", err)\n\t\t}\n\t\treturn defaultPromptConfig, nil\n\t}\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn PromptConfig{}, fmt.Errorf(\"error reading prompt config file: %v\", err)\n\t}\n\n\tvar config PromptConfig\n\terr = json.Unmarshal(data, &config)\n\tif err != nil {\n\t\treturn PromptConfig{}, fmt.Errorf(\"error unmarshalling prompt config: %v\", err)\n\t}\n\treturn config, nil\n}\n\n// ModifyPrompt updates the prompt configuration for a given domain.\n// It reads the existing prompt configuration, modifies the specified fields, and saves the updated configuration back to the file.","sourceCodeStart":777,"sourceCodeEnd":813,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/askai/project.go#L777-L813","documentation":"GetPrompt fails at project.go:795 when os.WriteFile cannot create prompt_config.json (mode 0644) in PRODUCT_CONFIG_PATH/<domain>/ while initializing the default prompt config for a domain that has no config file yet. os.WriteFile does not create parent directories, so a missing domain directory is the dominant cause; permission, read-only-filesystem, and disk-space problems produce the same error. The wrapped *fs.PathError identifies the exact path and errno.","triggerScenarios":"First call to GetPrompt(domain) (or ModifyPrompt which calls it) for a domain whose directory PRODUCT_CONFIG_PATH/<domain> was never created, when the process lacks write permission on that path, or when the config volume is read-only/full.","commonSituations":"Newly onboarded domain where no other config file has been written yet (nothing created the domain dir); app running as unprivileged container user while config dir is root-owned; docker-compose config volume mounted read-only; host disk full.","solutions":["Create the directory before writing: os.MkdirAll(filepath.Dir(filename), 0755) in the default-config branch.","Fix permissions/ownership on PRODUCT_CONFIG_PATH so the service user can create files.","Confirm the config volume is mounted read-write and has free space.","Inspect the wrapped *fs.PathError errno (ENOENT/EACCES/ENOSPC/EROFS) to pick the right fix."],"exampleFix":"// before\ndefaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, \"\", \"  \")\nif err != nil { ... }\nerr = os.WriteFile(filename, defaultPromptJson, 0644)\n// after\ndefaultPromptJson, err := json.MarshalIndent(defaultPromptConfig, \"\", \"  \")\nif err != nil { ... }\nif err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {\n    return PromptConfig{}, fmt.Errorf(\"error creating config dir: %v\", err)\n}\nif err := os.WriteFile(filename, defaultPromptJson, 0644); err != nil {\n    return PromptConfig{}, fmt.Errorf(\"error saving default prompt config file: %v\", err)\n}","handlingStrategy":"validation","validationCode":"filename := fmt.Sprintf(PRODUCT_CONFIG_PATH+\"/%s/prompt_config.json\", domain)\nif err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {\n    return fmt.Errorf(\"cannot create config dir: %v\", err)\n}\nprobe := filepath.Join(filepath.Dir(filename), \".write_probe\")\nif err := os.WriteFile(probe, nil, 0644); err != nil {\n    return fmt.Errorf(\"dir not writable: %v\", err)\n}\nos.Remove(probe)","typeGuard":"func canCreateIn(dir string) bool {\n    probe := filepath.Join(dir, \".probe\")\n    if err := os.WriteFile(probe, nil, 0644); err != nil {\n        return false\n    }\n    os.Remove(probe)\n    return true\n}","tryCatchPattern":"cfg, err := askai.GetPrompt(domain)\nif err != nil {\n    var perr *fs.PathError\n    if errors.As(err, &perr) {\n        switch {\n        case errors.Is(perr.Err, fs.ErrNotExist):\n            log.Printf(\"config dir %s missing — run MkdirAll during domain setup\", perr.Path)\n        case errors.Is(perr.Err, fs.ErrPermission):\n            log.Printf(\"permission denied on %s — fix volume ownership\", perr.Path)\n        default:\n            log.Printf(\"default prompt write failed: %v\", err)\n        }\n    }\n    return err\n}","preventionTips":["Create the domain config directory at domain-onboarding time, not lazily on first read.","Mount config volumes read-write and grant the service user ownership.","Check disk space and mount flags (rw vs ro) after deployment.","Treat the wrapped fs.Err errno (ENOENT/EACCES/ENOSPC/EROFS) to choose the fix."],"tags":["go","filesystem","permissions","config"],"backgroundTag":"file-write-permission-denied","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}