// Command sshdir refuses any manifest but modules/ssh-client naming a path under ~/.ssh (novox/hq ADR 0415 §6, // issue 1020): ssh-client owns ~/.ssh and sweeps what it does not know, so a path another module placed there // would be moved out at the next sweep. A module that needs something there provides it to ssh-client instead. // // go run . // // Every string of a manifest is read, wherever it stands, except prose and a file's text (Prose): a path, a // place, a volume. Exits 1 on a finding. package main import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" ) // SSHModule is the one module allowed to name a path under ~/.ssh. const SSHModule = "ssh-client" // underSSH is a path under ~/.ssh, or ~/.ssh itself, however the home is spelled: ${machine:account-home}/.ssh, // ~/.ssh, .ssh/config, or a volume's host side (…/.ssh:/root/.ssh). var underSSH = regexp.MustCompile(`(^|[/~])\.ssh($|[/:])`) // Prose are the keys whose strings are words or a file's text, not a path: a template may say ~/.ssh in a comment. var Prose = map[string]bool{"description": true, "why": true, "does": true, "template": true, "content": true, "title": true} // Finding is one path under ~/.ssh in a manifest other than ssh-client's. type Finding struct { Module string Key string Value string } // Check reads every manifest under root/modules and answers which paths it refuses. func Check(root string) ([]Finding, error) { manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json")) if err != nil { return nil, err } sort.Strings(manifests) var out []Finding for _, path := range manifests { raw, err := os.ReadFile(path) if err != nil { return nil, err } var m any if err := json.Unmarshal(raw, &m); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } module := filepath.Base(filepath.Dir(path)) if obj, ok := m.(map[string]any); ok { if name, ok := obj["module"].(string); ok && name != "" { module = name } } if module == SSHModule { continue } walk(m, "", func(key, value string) { out = append(out, Finding{Module: module, Key: key, Value: value}) }) } return out, nil } // walk calls found for every string under v, outside Prose, that names a path under ~/.ssh. func walk(v any, key string, found func(key, value string)) { switch x := v.(type) { case map[string]any: keys := make([]string, 0, len(x)) for k := range x { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { if !Prose[k] { walk(x[k], k, found) } } case []any: for _, e := range x { walk(e, key, found) } case string: if underSSH.MatchString(x) { found(key, x) } } } func main() { root := "." if len(os.Args) > 1 { root = os.Args[1] } found, err := Check(root) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } for _, f := range found { fmt.Printf("%s names %q (%s) under ~/.ssh: only %s may, and the sweep moves anything else out (novox/hq ADR 0415 §6)\n", f.Module, f.Value, f.Key, SSHModule) } if len(found) > 0 { os.Exit(1) } fmt.Println("~/.ssh: named only by ssh-client") }