MCPcopy Create free account
hub / github.com/codehamr/codehamr / Bootstrap

Function Bootstrap

internal/config/config.go:118–207  ·  view source on GitHub ↗

Bootstrap returns the config for the current project, creating .codehamr/ and config.yaml on first use. config.yaml is never overwritten; the prompt is embedded, never written to disk. The directory check uses Lstat (not Stat) and refuses a pre-existing .codehamr that isn't a real directory: a syml

(projectRoot string)

Source from the content-addressed store, hash-verified

116// .codehamr that isn't a real directory: a symlink there would let a co-tenant
117// redirect config.yaml to an attacker path, planting a models.<name>.url that
118// proxies the hamrpass key on the next dial-out.
119func Bootstrap(projectRoot string) (*Config, bool, error) {
120 dir := filepath.Join(projectRoot, DirName)
121 created := false
122 info, err := os.Lstat(dir)
123 switch {
124 case err == nil:
125 if info.Mode()&os.ModeSymlink != 0 {
126 return nil, false, fmt.Errorf("%s: refuses to follow symlink, remove or replace with a real directory", dir)
127 }
128 if !info.IsDir() {
129 return nil, false, fmt.Errorf("%s: exists but is not a directory", dir)
130 }
131 // Tighten a pre-existing loose dir (created by an older release or by
132 // hand): same upgrade-path rationale as Save's fresh-temp-inode trick
133 // for config.yaml, applied to the directory the threat comment below
134 // is about. Best-effort; a failure here shouldn't block launch.
135 if info.Mode().Perm() != 0o700 {
136 _ = os.Chmod(dir, 0o700)
137 }
138 case errors.Is(err, os.ErrNotExist):
139 // 0o700: config.yaml may carry the hamrpass key (a long-lived bearer
140 // token). A world-listable dir lets other local users spot it and probe
141 // for the key. Only the project owner should read here.
142 if err := os.MkdirAll(dir, 0o700); err != nil {
143 return nil, false, err
144 }
145 created = true
146 default:
147 return nil, false, err
148 }
149
150 cfgPath := filepath.Join(dir, "config.yaml")
151 // Same symlink defence as the directory check: a symlinked config.yaml
152 // could redirect the read (which config we honour) or the write (clobbering
153 // an arbitrary user-writable file with the seed). Refuse with a clear error.
154 if li, err := os.Lstat(cfgPath); err == nil && li.Mode()&os.ModeSymlink != 0 {
155 return nil, false, fmt.Errorf("%s: refuses to follow symlink, remove or replace with a real file", cfgPath)
156 }
157 var cfg *Config
158 if b, err := os.ReadFile(cfgPath); err == nil {
159 cfg = &Config{} // do NOT merge Default here; strict means strict
160 dec := yaml.NewDecoder(bytes.NewReader(b))
161 dec.KnownFields(true)
162 if err := dec.Decode(cfg); err != nil {
163 return nil, false, fmt.Errorf("config.yaml: %w", err)
164 }
165 } else if errors.Is(err, os.ErrNotExist) {
166 cfg = Default()
167 if err := writeYAML(cfgPath, cfg); err != nil {
168 return nil, false, err
169 }
170 } else {
171 return nil, false, err
172 }
173 cfg.Dir = dir
174
175 // YAML `models: { name: ~ }` decodes to a nil *Profile that would panic on

Calls 4

ModelNamesMethod · 0.95
DefaultFunction · 0.85
writeYAMLFunction · 0.85
IsCloudProfileFunction · 0.85