Files
Agent-System/cmd/document-content-reconcile/safety.go
T
weichenandCursor e3b72ad64b fix(cli): require positive live-dev identity and exclusive backups
Live apply now accepts only :5433/agent_system_dev plus a data/dev
host root, maps stored work dirs fail-closed, and reports unique
O_EXCL backups after a parent-directory fsync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 02:30:11 +00:00

172 lines
4.5 KiB
Go

package main
import (
"fmt"
"net/url"
"path/filepath"
"strings"
)
const (
envDev = "dev"
confirmRepairSingleDev = "REPAIR_SINGLE_DEV_DOC"
devDatabaseName = "agent_system_dev"
devPort = "5433"
)
type cliConfig struct {
DSN string
RepoRoot string
DocID string
BackupDir string
Apply bool
Live bool
Environment string
Confirm string
StoredDataRoot string
HostDataRoot string
}
func validateCLI(cfg cliConfig) error {
if strings.TrimSpace(cfg.DSN) == "" {
return fmt.Errorf("dsn or TEST_DATABASE_URL required")
}
if cfg.Apply && strings.TrimSpace(cfg.BackupDir) == "" {
return fmt.Errorf("--backup-dir is required with --apply")
}
if (strings.TrimSpace(cfg.StoredDataRoot) == "") != (strings.TrimSpace(cfg.HostDataRoot) == "") {
return fmt.Errorf("--stored-data-root and --host-data-root must be set together")
}
if !cfg.Live {
if sharedDSNEnv(cfg.DSN) != "" || firstSharedPathEnv(cfg.RepoRoot, cfg.BackupDir, cfg.StoredDataRoot, cfg.HostDataRoot) != "" {
return fmt.Errorf("refusing shared/env data path or DSN; use isolated TEST_DATABASE_URL, or pass --live --environment dev --doc-id and confirm")
}
return nil
}
if strings.TrimSpace(cfg.Environment) != envDev {
return fmt.Errorf("live mode only allows --environment %s", envDev)
}
if strings.TrimSpace(cfg.DocID) == "" {
return fmt.Errorf("live mode forbids batch operations; --doc-id is required")
}
if err := requireLiveDevDSN(cfg.DSN); err != nil {
return err
}
if strings.TrimSpace(cfg.StoredDataRoot) == "" || strings.TrimSpace(cfg.HostDataRoot) == "" {
return fmt.Errorf("live mode requires --stored-data-root and --host-data-root")
}
if !isDevDataRoot(cfg.HostDataRoot) {
return fmt.Errorf("live --environment dev requires --host-data-root to be a data/dev directory")
}
if env := sharedPathEnv(cfg.HostDataRoot); env != envDev {
return fmt.Errorf("live mode refuses host-data-root that is not positively data/dev")
}
if cfg.Apply && strings.TrimSpace(cfg.Confirm) != confirmRepairSingleDev {
return fmt.Errorf("live apply requires --confirm %s", confirmRepairSingleDev)
}
return nil
}
func requireLiveDevDSN(dsn string) error {
port, db, err := inspectPostgresDSN(dsn)
if err != nil {
return fmt.Errorf("live mode: %w", err)
}
if port != devPort || db != devDatabaseName {
return fmt.Errorf("live --environment dev requires DSN port :%s and database %s (got port=%q db=%q)", devPort, devDatabaseName, port, db)
}
return nil
}
func inspectPostgresDSN(dsn string) (port, db string, err error) {
dsn = strings.TrimSpace(dsn)
if dsn == "" {
return "", "", fmt.Errorf("empty dsn")
}
if strings.Contains(dsn, "://") {
u, perr := url.Parse(dsn)
if perr != nil {
return "", "", fmt.Errorf("invalid dsn: %w", perr)
}
return u.Port(), strings.Trim(u.Path, "/"), nil
}
for _, part := range strings.Fields(dsn) {
k, v, ok := strings.Cut(part, "=")
if !ok {
continue
}
switch k {
case "port":
port = v
case "dbname":
db = v
}
}
if port == "" && strings.Contains(dsn, ":"+devPort) {
port = devPort
}
if db == "" {
return "", "", fmt.Errorf("dsn is not a recognizable postgres URL or keyword string")
}
return port, db, nil
}
func isDevDataRoot(p string) bool {
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(p)))
return clean == "data/dev" || strings.HasSuffix(clean, "/data/dev")
}
func sharedDSNEnv(dsn string) string {
port, _, err := inspectPostgresDSN(dsn)
if err != nil {
switch {
case strings.Contains(dsn, ":5433"):
return envDev
case strings.Contains(dsn, ":5434"):
return "test"
case strings.Contains(dsn, ":5435"):
return "main"
default:
return ""
}
}
switch port {
case "5433":
return envDev
case "5434":
return "test"
case "5435":
return "main"
default:
return ""
}
}
func firstSharedPathEnv(paths ...string) string {
for _, p := range paths {
if env := sharedPathEnv(p); env != "" {
return env
}
}
return ""
}
func sharedPathEnv(p string) string {
if strings.TrimSpace(p) == "" {
return ""
}
clean := filepath.ToSlash(filepath.Clean(p))
switch {
case clean == "data/dev" || strings.HasSuffix(clean, "/data/dev") || strings.Contains(clean, "/data/dev/"):
return envDev
case clean == "data/test" || strings.HasSuffix(clean, "/data/test") || strings.Contains(clean, "/data/test/"):
return "test"
case clean == "data/main" || strings.HasSuffix(clean, "/data/main") || strings.Contains(clean, "/data/main/"):
return "main"
default:
return ""
}
}