projection: begin the exact-path public epoch

The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally.

Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
This commit is contained in:
Fimeg 2026-09-08 21:59:33 -04:00
commit 67e26be2d9
838 changed files with 157302 additions and 0 deletions

View file

@ -0,0 +1,37 @@
// Package instancelock prevents multiple agent processes from running
// concurrently on the same host by holding an exclusive lock on a
// well-known path (Unix: flock on /var/lib/.../agent.lock; Windows:
// named kernel mutex Global\RedFlagAgent_v1 + per-user file lock).
// The lock is released when the process exits (the OS cleans up).
// If the lock cannot be acquired, the instance should exit immediately
// rather than share config.json and renewal state.
package instancelock
import (
"path/filepath"
)
// LockPath returns the well-known path for the Unix instance lockfile.
// Windows uses a named kernel mutex instead; this function is only called
// from lock_unix.go and is referenced here so the package compiles on
// Windows.
func LockPath() string {
return filepath.Join("/var/lib/redflag/agent/state", "agent.lock")
}
// Acquire attempts to acquire an exclusive instance lock. It returns a
// release function and nil on success, or an error if another agent
// process is already running on this host.
//
// The release function must be called on graceful shutdown.
// On process crash the OS releases the lock automatically:
// - Unix: the kernel closes the flock fd on process exit.
// - Windows: the kernel transitions the mutex to "abandoned" state;
// a crashed owner's mutex is acquired cleanly by the next waiter.
func Acquire() (release func(), err error) {
return acquireLock()
}
// noopRelease is a safe no-op for platforms that don't need cleanup.
func noopRelease() {}

View file

@ -0,0 +1,45 @@
//go:build linux || darwin || freebsd
package instancelock
import (
"fmt"
"os"
"path/filepath"
"syscall"
)
// acquireLock opens (or creates) the lockfile and takes an exclusive
// flock. The lock is released when the process exits (the kernel
// closes the fd, which releases the flock).
func acquireLock() (func(), error) {
path := LockPath()
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("instancelock: mkdir %s: %w", dir, err)
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return nil, fmt.Errorf("instancelock: open %s: %w", path, err)
}
fd := int(f.Fd())
if err := syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
f.Close()
return nil, fmt.Errorf("instancelock: %s is locked by another process: %w", path, err)
}
// Write our PID so operators can see who holds the lock.
_, _ = f.WriteAt([]byte(fmt.Sprintf("%d\n", os.Getpid())), 0)
_ = f.Truncate(64)
release := func() {
_ = f.Close()
// Don't remove the file — leaving it is harmless and prevents a
// TOCTOU race where a fresh open might get an unlocked fd before
// we flock it.
}
return release, nil
}

View file

@ -0,0 +1,127 @@
//go:build windows
package instancelock
import (
"fmt"
"os"
"path/filepath"
"sync"
"golang.org/x/sys/windows"
)
var (
// mu guards handle — the OS mutex handle must be closed exactly once,
// but it must outlive the caller's release function (it's process-wide).
mu sync.Mutex
handle windows.Handle
)
// lockName is the well-known kernel-object name. The Global\ prefix makes
// it visible across all sessions (including the service session), which is
// necessary because the agent may run both as a service and as a console
// process under different sessions on the same host.
const lockName = `Global\RedFlagAgent_v1`
// acquireLock opens a named kernel mutex and attempts to acquire it
// without blocking. If the mutex is held by another process, we fail
// immediately. If it doesn't exist, CreateMutex creates it for us.
//
// Unlike the Unix flock (which lives on an fd scoped to the file system
// path), a Windows mutex is purely a kernel object — there is no file
// to write a PID or leak. The name is the key: any process on the system
// (service, console, WSL bridge) that opens the same name gets the same
// mutex object.
func acquireLock() (func(), error) {
mu.Lock()
defer mu.Unlock()
// If we already hold the lock in this process (shouldn't happen in
// normal use, but guard it), don't re-acquire.
if handle != 0 {
return noopRelease, nil
}
name, err := windows.UTF16PtrFromString(lockName)
if err != nil {
return nil, fmt.Errorf("instancelock: utf16: %w", err)
}
// CreateMutex opens or creates the named mutex. It does NOT set the
// initial ownership — we do that with WaitForSingleObject below.
h, err := windows.CreateMutex(nil, false, name)
if err != nil {
return nil, fmt.Errorf("instancelock: CreateMutex: %w", err)
}
// Attempt to acquire with zero timeout (non-blocking).
// WAIT_OBJECT_0 (0) = we got it.
// WAIT_ABANDONED (128) = previous owner died holding it — it's ours.
// WAIT_TIMEOUT (258) = someone else has it.
switch waitResult, _ := windows.WaitForSingleObject(h, 0); waitResult {
case 0, windows.WAIT_ABANDONED:
handle = h
case 258: // WAIT_TIMEOUT
_ = windows.CloseHandle(h)
return nil, fmt.Errorf("instancelock: another agent process is already running on this host (locked mutex: %s)", lockName)
default:
_ = windows.CloseHandle(h)
return nil, fmt.Errorf("instancelock: WaitForSingleObject failed on %s", lockName)
}
// Release is called on graceful shutdown. If the process crashes or
// is killed, the kernel releases the mutex automatically — unlike
// Unix where flock is tied to the fd and the fd closes on exit, a
// Windows mutex held by a dead thread transitions to "abandoned"
// and the next waiter acquires it cleanly.
release := func() {
mu.Lock()
defer mu.Unlock()
if handle != 0 {
_ = windows.ReleaseMutex(handle)
_ = windows.CloseHandle(handle)
handle = 0
}
}
// Also create a per-user lockfile under APPDATA to catch the case
// where the same user runs two console-mode agents without going
// through the SCM. The kernel mutex covers cross-session; the file
// lock covers same-user-duplicates where both instances open the
// same file.
localAppData := os.Getenv("LOCALAPPDATA")
if localAppData != "" {
lockFilePath := filepath.Join(localAppData, "RedFlag", "agent.lock")
// Best-effort: if we can't write it, the kernel mutex still
// protects us across all sessions.
_ = os.MkdirAll(filepath.Dir(lockFilePath), 0755)
if f, fErr := os.OpenFile(lockFilePath, os.O_RDWR|os.O_CREATE, 0644); fErr == nil {
if lErr := lockFile(f); lErr == nil {
// Extend release to also close the file.
prevRelease := release
release = func() {
_ = f.Close()
prevRelease()
}
} else {
_ = f.Close()
}
}
}
return release, nil
}
// lockFile takes an advisory lock on an *os.File using LockFileEx (the
// Windows equivalent of flock). This is belt-and-suspenders with the
// kernel mutex — both must be acquired for the lock to count.
func lockFile(f *os.File) error {
ol := &windows.Overlapped{}
return windows.LockFileEx(
windows.Handle(f.Fd()),
windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY,
0, 1, 0, ol,
)
}