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>
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
const (
|
||||
envDev = "dev"
|
||||
confirmRepairSingleDev = "REPAIR_SINGLE_DEV_DOC"
|
||||
devDatabaseName = "agent_system_dev"
|
||||
devPort = "5433"
|
||||
)
|
||||
|
||||
type cliConfig struct {
|
||||
@@ -31,15 +34,12 @@ func validateCLI(cfg cliConfig) error {
|
||||
if cfg.Apply && strings.TrimSpace(cfg.BackupDir) == "" {
|
||||
return fmt.Errorf("--backup-dir is required with --apply")
|
||||
}
|
||||
if (cfg.StoredDataRoot == "") != (cfg.HostDataRoot == "") {
|
||||
if (strings.TrimSpace(cfg.StoredDataRoot) == "") != (strings.TrimSpace(cfg.HostDataRoot) == "") {
|
||||
return fmt.Errorf("--stored-data-root and --host-data-root must be set together")
|
||||
}
|
||||
|
||||
dsnEnv := sharedDSNEnv(cfg.DSN)
|
||||
pathEnv := firstSharedPathEnv(cfg.RepoRoot, cfg.BackupDir, cfg.StoredDataRoot, cfg.HostDataRoot)
|
||||
|
||||
if !cfg.Live {
|
||||
if dsnEnv != "" || pathEnv != "" {
|
||||
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
|
||||
@@ -51,11 +51,17 @@ func validateCLI(cfg cliConfig) error {
|
||||
if strings.TrimSpace(cfg.DocID) == "" {
|
||||
return fmt.Errorf("live mode forbids batch operations; --doc-id is required")
|
||||
}
|
||||
if dsnEnv != "" && dsnEnv != envDev {
|
||||
return fmt.Errorf("live mode refuses shared %s DSN", dsnEnv)
|
||||
if err := requireLiveDevDSN(cfg.DSN); err != nil {
|
||||
return err
|
||||
}
|
||||
if pathEnv != "" && pathEnv != envDev {
|
||||
return fmt.Errorf("live mode refuses shared %s data path", pathEnv)
|
||||
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)
|
||||
@@ -63,13 +69,75 @@ func validateCLI(cfg cliConfig) error {
|
||||
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 {
|
||||
switch {
|
||||
case strings.Contains(dsn, ":5433"):
|
||||
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 strings.Contains(dsn, ":5434"):
|
||||
case "5434":
|
||||
return "test"
|
||||
case strings.Contains(dsn, ":5435"):
|
||||
case "5435":
|
||||
return "main"
|
||||
default:
|
||||
return ""
|
||||
@@ -90,20 +158,14 @@ func sharedPathEnv(p string) string {
|
||||
return ""
|
||||
}
|
||||
clean := filepath.ToSlash(filepath.Clean(p))
|
||||
needles := []struct {
|
||||
env string
|
||||
tokens []string
|
||||
}{
|
||||
{envDev, []string{"/data/dev", "data/dev"}},
|
||||
{"test", []string{"/data/test", "data/test"}},
|
||||
{"main", []string{"/data/main", "data/main"}},
|
||||
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 ""
|
||||
}
|
||||
for _, n := range needles {
|
||||
for _, token := range n.tokens {
|
||||
if strings.Contains(clean, token) {
|
||||
return n.env
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -19,21 +19,29 @@ func TestValidateCLI_DefaultRefusesSharedDev(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCLI_LiveApplyRequiresConfirmAndDocID(t *testing.T) {
|
||||
base := cliConfig{
|
||||
func validLiveApply() cliConfig {
|
||||
return cliConfig{
|
||||
DSN: "postgres://app:x@127.0.0.1:5433/agent_system_dev?sslmode=disable",
|
||||
RepoRoot: "/repo/data/dev",
|
||||
DocID: "doc_proj143_intake",
|
||||
Apply: true,
|
||||
Live: true,
|
||||
Environment: "dev",
|
||||
Confirm: confirmRepairSingleDev,
|
||||
BackupDir: "/tmp/backup",
|
||||
StoredDataRoot: "/app/data",
|
||||
HostDataRoot: "/repo/data/dev",
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCLI_LiveApplyRequiresConfirmAndDocID(t *testing.T) {
|
||||
base := validLiveApply()
|
||||
base.DocID = ""
|
||||
if err := validateCLI(base); err == nil {
|
||||
t.Fatal("live apply without --doc-id must fail")
|
||||
}
|
||||
base.DocID = "doc_proj143_intake"
|
||||
base.Confirm = ""
|
||||
if err := validateCLI(base); err == nil {
|
||||
t.Fatal("live apply without confirm must fail")
|
||||
}
|
||||
@@ -47,18 +55,67 @@ func TestValidateCLI_LiveApplyRequiresConfirmAndDocID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCLI_LiveRequiresPositiveDevDSNAndHostRoot(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mut func(*cliConfig)
|
||||
}{
|
||||
{"port 5433 but wrong db", func(c *cliConfig) {
|
||||
c.DSN = "postgres://app:x@127.0.0.1:5433/agent_itest?sslmode=disable"
|
||||
}},
|
||||
{"dev db name but isolated port", func(c *cliConfig) {
|
||||
c.DSN = "postgres://app:x@127.0.0.1:55432/agent_system_dev?sslmode=disable"
|
||||
}},
|
||||
{"empty dsn env must not pass live", func(c *cliConfig) {
|
||||
c.DSN = "postgres://app:x@127.0.0.1:55432/agent_itest?sslmode=disable"
|
||||
}},
|
||||
{"host root not data/dev", func(c *cliConfig) {
|
||||
c.HostDataRoot = "/tmp/isolated-host"
|
||||
}},
|
||||
{"missing mapping roots", func(c *cliConfig) {
|
||||
c.StoredDataRoot = ""
|
||||
c.HostDataRoot = ""
|
||||
}},
|
||||
{"host root is data/test", func(c *cliConfig) {
|
||||
c.HostDataRoot = "/repo/data/test"
|
||||
}},
|
||||
{"keyword dsn missing port", func(c *cliConfig) {
|
||||
c.DSN = "host=127.0.0.1 dbname=agent_system_dev"
|
||||
}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := validLiveApply()
|
||||
tc.mut(&cfg)
|
||||
if err := validateCLI(cfg); err == nil {
|
||||
t.Fatalf("%s: live must fail closed", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCLI_LiveKeywordDSNAccepted(t *testing.T) {
|
||||
cfg := validLiveApply()
|
||||
cfg.DSN = "host=127.0.0.1 port=5433 dbname=agent_system_dev sslmode=disable"
|
||||
if err := validateCLI(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCLI_LiveForbidsBatchAndNonDev(t *testing.T) {
|
||||
err := validateCLI(cliConfig{
|
||||
DSN: "postgres://app:x@127.0.0.1:5434/agent_system_test?sslmode=disable",
|
||||
Live: true,
|
||||
Environment: "dev",
|
||||
DocID: "doc_x",
|
||||
DSN: "postgres://app:x@127.0.0.1:5434/agent_system_test?sslmode=disable",
|
||||
Live: true,
|
||||
Environment: "dev",
|
||||
DocID: "doc_x",
|
||||
StoredDataRoot: "/app/data",
|
||||
HostDataRoot: "/repo/data/dev",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("live mode must refuse :5434")
|
||||
}
|
||||
err = validateCLI(cliConfig{
|
||||
DSN: "postgres://app:x@127.0.0.1:55432/itest?sslmode=disable",
|
||||
DSN: "postgres://app:x@127.0.0.1:5433/agent_system_dev?sslmode=disable",
|
||||
Live: true,
|
||||
Environment: "test",
|
||||
DocID: "doc_x",
|
||||
@@ -66,15 +123,6 @@ func TestValidateCLI_LiveForbidsBatchAndNonDev(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("live mode must refuse non-dev environment")
|
||||
}
|
||||
err = validateCLI(cliConfig{
|
||||
DSN: "postgres://app:x@127.0.0.1:5433/agent_system_dev?sslmode=disable",
|
||||
RepoRoot: "/repo/data/dev",
|
||||
Live: true,
|
||||
Environment: "dev",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("live mode must refuse batch without --doc-id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCLI_IsolatedDefaultAllowsApply(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user