Add evidence store, intent store and the fluid CLI
Some checks failed
ci / build (push) Has been cancelled
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
This commit is contained in:
parent
791e419973
commit
d52dcc92a9
11 changed files with 1836 additions and 3 deletions
144
cmd/fluid/main.go
Normal file
144
cmd/fluid/main.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue