Some checks failed
ci / build (push) Has been cancelled
Completes FLUID-WP-0003. The evidence store is append-only at the database, not by convention in Go: a trigger blocks UPDATE and DELETE on fluid_events in both SQLite and Postgres, so the guarantee binds a psql session and the CLI equally, not just callers who go through the Go API. Records are derived summary state and may be superseded; the event log stays the authority. The intent store makes a recorded version immutable and content addressed, since reassigning what governed a revision after the fact would break the one audit question Blueprint 27 exists to answer. The unfilled InterfaceEvolutionIntent template is rejected rather than defaulted, because a template still listing every FLUID-N mode has not been completed and defaulting it would pick a permissive authority by accident. The CLI reads the evidence store directly rather than through the control-plane API, so an operator can reconstruct history when the control plane is down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
144 lines
3.4 KiB
Go
144 lines
3.4 KiB
Go
// Command fluid is the FLUID operator CLI.
|
|
//
|
|
// It reads and writes the evidence store directly rather than going through the
|
|
// control-plane API, so that an operator can still inspect an interface's
|
|
// history when the control plane is down. Auditability must not depend on the
|
|
// availability of the thing being audited.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const usage = `fluid — FLUID operator CLI
|
|
|
|
Usage:
|
|
fluid [--store PATH] [--interface ID] <command> [arguments]
|
|
|
|
Commands:
|
|
intent put Record an interface evolution intent version
|
|
intent activate Make a recorded version the governing intent
|
|
intent show Show the active or a named intent version
|
|
revision publish Publish a signed revision descriptor
|
|
revision list List published revisions
|
|
revision show Show one revision descriptor
|
|
policy put Install a routing policy
|
|
policy show Show the current routing policy
|
|
events Show audit events
|
|
telemetry Show recorded telemetry
|
|
audit trace Reconstruct the history behind a revision
|
|
|
|
Global flags:
|
|
--store PATH Evidence store path (default $FLUID_STORE or ./fluid.db)
|
|
--interface ID Interface identifier (default $FLUID_INTERFACE)
|
|
`
|
|
|
|
func main() {
|
|
if err := run(os.Args[1:]); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
os.Exit(0)
|
|
}
|
|
fmt.Fprintln(os.Stderr, "fluid:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type globals struct {
|
|
store string
|
|
iface string
|
|
remaining []string
|
|
}
|
|
|
|
func parseGlobals(args []string) (globals, error) {
|
|
g := globals{
|
|
store: envOr("FLUID_STORE", "fluid.db"),
|
|
iface: os.Getenv("FLUID_INTERFACE"),
|
|
}
|
|
|
|
i := 0
|
|
for i < len(args) {
|
|
switch args[i] {
|
|
case "--store":
|
|
if i+1 >= len(args) {
|
|
return g, errors.New("--store needs a value")
|
|
}
|
|
g.store, i = args[i+1], i+2
|
|
case "--interface":
|
|
if i+1 >= len(args) {
|
|
return g, errors.New("--interface needs a value")
|
|
}
|
|
g.iface, i = args[i+1], i+2
|
|
case "-h", "--help", "help":
|
|
fmt.Print(usage)
|
|
return g, flag.ErrHelp
|
|
default:
|
|
if strings.HasPrefix(args[i], "--") {
|
|
return g, fmt.Errorf("unknown global flag %q", args[i])
|
|
}
|
|
g.remaining = args[i:]
|
|
return g, nil
|
|
}
|
|
}
|
|
g.remaining = nil
|
|
return g, nil
|
|
}
|
|
|
|
func run(args []string) error {
|
|
g, err := parseGlobals(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(g.remaining) == 0 {
|
|
fmt.Print(usage)
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cmd, rest := g.remaining[0], g.remaining[1:]
|
|
|
|
switch cmd {
|
|
case "intent":
|
|
return runIntent(ctx, g, rest)
|
|
case "revision":
|
|
return runRevision(ctx, g, rest)
|
|
case "policy":
|
|
return runPolicy(ctx, g, rest)
|
|
case "events":
|
|
return runEvents(ctx, g, rest)
|
|
case "telemetry":
|
|
return runTelemetry(ctx, g, rest)
|
|
case "audit":
|
|
return runAudit(ctx, g, rest)
|
|
default:
|
|
return fmt.Errorf("unknown command %q (try `fluid help`)", cmd)
|
|
}
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// requireInterface fails clearly rather than silently operating on the wrong
|
|
// interface's history.
|
|
func (g globals) requireInterface() (string, error) {
|
|
if g.iface == "" {
|
|
return "", errors.New("no interface selected: pass --interface or set FLUID_INTERFACE")
|
|
}
|
|
return g.iface, nil
|
|
}
|
|
|
|
func (g globals) storePath() string {
|
|
if abs, err := filepath.Abs(g.store); err == nil {
|
|
return abs
|
|
}
|
|
return g.store
|
|
}
|