// Package bootstrap 承担实例启动所有权(KTD17 启动顺序中的 // “取得本实例启动所有权”:身份标记、单启锁与可写根校验)。 // 失败一律关闭:身份不符、锁被占、目录异常都拒绝启动。 package bootstrap import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "syscall" "time" "chat-agent-system/internal/config" ) // InstanceMarker 是数据根内的实例身份标记:防止把别的实例的数据目录 // 当作自己的空库/新实例使用(KTD18 instance marker 语义)。 type InstanceMarker struct { InstanceID string `json:"instance_id"` CreatedAt string `json:"created_at"` Version int `json:"marker_version"` } const markerVersion = 1 // InstanceOwnership 是启动所有权的持有凭据:进程存活期间保持锁。 // Release 由调用方在进程退出路径调用(通常无需主动:进程退出自动释放)。 type InstanceOwnership struct { InstanceID string DataDir string lockFile *os.File } func (o *InstanceOwnership) Release() { if o != nil && o.lockFile != nil { _ = syscall.Flock(int(o.lockFile.Fd()), syscall.LOCK_UN) _ = o.lockFile.Close() o.lockFile = nil } } // EnsureInstance 校验并取得实例启动所有权: // 1. 归一化可写根并校验重叠/合法性; // 2. 数据根 instance-marker.json 建立或核对身份(不同实例拒绝,须显式迁移/收养); // 3. 数据根 instance.lock 非阻塞 flock,防止同机误重复启动(不依赖任何治理工具)。 // // 只做本机文件级防护;跨主机共享可写文件树不在支持范围(KTD17)。 func EnsureInstance(cfg *config.Config) (*InstanceOwnership, error) { if cfg == nil { return nil, fmt.Errorf("bootstrap: config required") } id := strings.TrimSpace(cfg.InstanceID) if id == "" { return nil, fmt.Errorf("bootstrap: INSTANCE_ID must not be empty") } if strings.ContainsAny(id, `/\:*?"<>| `) || id == "." || id == ".." { return nil, fmt.Errorf("bootstrap: INSTANCE_ID %q must not contain path/space characters", id) } dataDir, err := filepath.Abs(cfg.DataDir) if err != nil { return nil, fmt.Errorf("bootstrap: resolve data dir: %w", err) } if dataDir == string(filepath.Separator) { return nil, fmt.Errorf("bootstrap: data dir must not be filesystem root") } if err := checkWritableRoot(dataDir, "data_dir"); err != nil { return nil, err } if v := strings.TrimSpace(cfg.VaultStateRoot); v != "" { vault, err := filepath.Abs(v) if err != nil { return nil, fmt.Errorf("bootstrap: resolve vault root: %w", err) } if containsPath(dataDir, vault) || containsPath(vault, dataDir) { return nil, fmt.Errorf("bootstrap: vault root %s and data dir %s must not overlap", vault, dataDir) } } if err := os.MkdirAll(dataDir, 0o755); err != nil { return nil, fmt.Errorf("bootstrap: create data dir: %w", err) } if err := ensureMarker(dataDir, id); err != nil { return nil, err } lockPath := filepath.Join(dataDir, "instance.lock") lf, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, fmt.Errorf("bootstrap: open instance lock: %w", err) } if err := syscall.Flock(int(lf.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { lf.Close() return nil, fmt.Errorf("bootstrap: instance %q already running on this machine (lock %s held)", id, lockPath) } return &InstanceOwnership{InstanceID: id, DataDir: dataDir, lockFile: lf}, nil } func checkWritableRoot(abs, name string) error { // 符号链接解析后复核,防止用链接把两个实例指向同一物理根。 real, err := filepath.EvalSymlinks(abs) if err != nil { if !os.IsNotExist(err) { return fmt.Errorf("bootstrap: eval %s: %w", name, err) } // 目录尚不存在:父目录必须可解析(MkdirAll 随后创建) parent := filepath.Dir(abs) if _, err := os.Stat(parent); err != nil { return fmt.Errorf("bootstrap: %s parent missing: %w", name, err) } return nil } fi, err := os.Stat(real) if err != nil { return fmt.Errorf("bootstrap: stat %s: %w", name, err) } if !fi.IsDir() { return fmt.Errorf("bootstrap: %s %s is not a directory", name, abs) } return nil } // containsPath 报告 a 是否等于 b 或包含 b(按路径组件)。 func containsPath(a, b string) bool { if a == b { return true } return strings.HasPrefix(b, a+string(filepath.Separator)) } func ensureMarker(dataDir, id string) error { path := filepath.Join(dataDir, "instance-marker.json") raw, err := os.ReadFile(path) switch { case os.IsNotExist(err): m := InstanceMarker{InstanceID: id, CreatedAt: time.Now().UTC().Format(time.RFC3339), Version: markerVersion} b, _ := json.MarshalIndent(m, "", " ") if err := os.WriteFile(path, append(b, '\n'), 0o600); err != nil { return fmt.Errorf("bootstrap: write instance marker: %w", err) } return nil case err != nil: return fmt.Errorf("bootstrap: read instance marker: %w", err) } var m InstanceMarker if err := json.Unmarshal(raw, &m); err != nil { return fmt.Errorf("bootstrap: parse instance marker %s: %w", path, err) } if m.InstanceID != id { return fmt.Errorf("bootstrap: data dir %s belongs to instance %q, refusing to start as %q (adopt/migrate explicitly)", dataDir, m.InstanceID, id) } return nil }