sisyphus

package module
v0.1.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 7 Imported by: 0

README

sisyphus

CI

sisyphus is a small, app-agnostic Go toolkit for the plumbing every CLI or service ends up rewriting. It carries no application-specific types or names — the caller owns its config struct, namespaces, filenames, and identifiers and passes them in. It was extracted from munin but depends on nothing munin-specific.

go get github.com/codyconfer/sisyphus

The module is available through the standard Go module proxy and checksum database. DuckDB-backed packages require CGO (via github.com/marcboeker/go-duckdb/v2).

Packages

Package Responsibility
sisyphus (root) Manager facade for config reconciliation + package-level Backup/Restore.
sisyphus/config Home-dir resolution and parsing a YAML/JSON file into your struct (with env overrides).
sisyphus/configdb Versioned, name-keyed blob store in DuckDB (store_current + store_history) — the source of truth for file-backed state.
sisyphus/kv Generic namespaced key/value store in DuckDB, with an optional TTL column.
sisyphus/journal Generic activity log in DuckDB: nested parent/child runs + records, each with a free-form string attribute map.
sisyphus/secret Key escrow via the Bitwarden (bw) or 1Password (op) CLI, or the OS keyring.
sisyphus/backup tar archive + AES-256-GCM encrypt/decrypt/restore.
sisyphus/store Ad-hoc DuckDB file queries (read-only at the app layer).
sisyphus/sealed Encrypted credential store (AES-GCM over kv; key in OS keyring).
sisyphus/auth OAuth loopback · device flow · RunTool CLI helper.
sisyphus/mode Operating modes + injectable auth gate hooks.
sisyphus/lifecycle Home-dir install / clean / nuke primitives; shell hook runner (Scripts / Select / Run).
sisyphus/desktop OS desktop notifications (beeep). Untagged leaf — does not import daemon.
sisyphus/daemon Streaming core: poll/fan-in/dedupe, sockets (pipe-prefix param on Windows), cursors.
sisyphus/daemon/service OS service install/start/stop wrapper. Empty under nodaemon.
sisyphus/daemon/ui System tray. Empty under nodaemon.

Each sub-package is usable on its own. A nil *Store is a valid no-op across the DuckDB packages, so "disabled" and "open failed" behave uniformly.

App-agnostic by design

Everything application-specific is injected, never baked in:

  • Home dirconfig.Home(override, envVar, dirName) takes the env var and directory name.
  • Config file namesconfig.ReadFile(home, basenames...) (defaults to config.{yaml,yml,json} when none given).
  • Env prefixconfig.ParseInto(target, raw, format, envPrefix).
  • KV namespace — a parameter on every kv call.
  • Config DB filenameOptions.ConfigDBName (defaults to config.duckdb).
  • Keyring service namesecret.Resolve(backend, service) (defaults to "sisyphus"); backup threads it via BackupSpec.SecretService.
  • Backup file list + secret name — supplied on BackupSpec / RestoreSpec.

Usage

Config reconciliation

Manager makes DuckDB the source of truth for file-backed config, and never auto-imports — you decide via a Resolver when file and DB disagree.

ctx := context.Background()
m, err := sisyphus.Open(ctx, home, sisyphus.Options{}) // ModeBoth; ConfigDBName defaults to config.duckdb
if err != nil { /* ... */ }
defer m.Close()

raw, format, _ := config.ReadFile(home)
content, format, err := m.Reconcile(ctx, "config", raw, format, len(raw) > 0, myResolver)
// then: config.ParseInto(&myCfg, content, format, "MYAPP_")

Reconcile returns the DB content when file and DB match, and otherwise calls Resolver.Resolve with an Action (ActionImport / ActionUseFile / ActionUseDB). Manager.Current/Import/History cover the common config-DB operations; DB() exposes the underlying *configdb.Store for anything more.

Authorization gates

The mode package runs authorization policy supplied by your application; it does not decide who is authorized. Your GateHooks.Classify callback maps the current account state to:

  • AuthUnauthenticated — no valid identity or login.
  • AuthUnauthorized — authenticated, but missing a required approval, membership, scope, or onboarding step.
  • AuthAuthorized — fully allowed.

AllOrNothingAuth is not a global "require authentication" switch. It affects only an unauthorized CLI user: when CLIUnauthorized returns an error, AllOrNothingAuth: true propagates that error and blocks the command; AllOrNothingAuth: false discards it and allows the command to continue.

Mode and state Gate behavior
CLI, unauthenticated Runs CLIUnauthenticated; any error blocks.
CLI, unauthorized, default policy Runs CLIUnauthorized, discards its error, and continues.
CLI, unauthorized, all-or-nothing auth Runs CLIUnauthorized; any error blocks.
CLI, authorized Continues without calling an auth hook.
Serve or daemon, not authorized Runs the corresponding hook; return nil to warn and continue, or an error to block.
Serve or daemon, nodaemon build Returns ErrUnsupportedMode without calling any hook.
Deck, any state Always runs DeckRequire when that hook is provided.
err := mode.Gate(ctx, mode.ModeCLI, mode.GateHooks{
    Classify: func(ctx context.Context) mode.AuthState {
        return classifyMyAccount(ctx) // application-specific policy
    },
    CLIUnauthenticated: loginAndOnboard,
    CLIUnauthorized: func(context.Context) error {
        return errors.New("account is not approved")
    },
    AllOrNothingAuth: true,
})
if err != nil {
    return err // stop before running the command
}

The gate allows execution when Classify is nil, when the applicable hook is nil, or when a blocking hook returns nil. Applications requiring strict authorization should supply every relevant hook, return explicit denial errors, and stop whenever Gate returns an error. OAuth flows in auth establish credentials; the application still decides whether those credentials are authorized.

Daemon-free builds (nodaemon)

The nodaemon build tag compiles out everything that presumes a long-running background service, so an application can ship a CLI-only binary from the same source tree:

go build -tags nodaemon ./...
make test TAGS=nodaemon
Symbol Default build nodaemon build
mode.DaemonSupported true false
mode.Supported(m) true for every mode false for ModeServe / ModeDaemon
mode.Gate(ctx, m, hooks) Runs the hooks Wraps ErrUnsupportedMode for serve/daemon
daemon.Attached(prefix, name) Probes the socket when DaemonSupported Always false (gates on mode.DaemonSupported)
daemon/service, daemon/ui Full API Empty packages
desktop Full API Full API (untagged; import only when you want notifications)

DaemonSupported is a constant, so if !mode.DaemonSupported { … } is eliminated at compile time and the daemon half of your program can be dropped from the binary. daemon.Attached is the capability-aware form of daemon.IsListening — it returns false when !mode.DaemonSupported, otherwise delegates to IsListening. Gate optional UI and features on Attached, and use IsListening only when you want a raw probe regardless of build. mode is the sole build-tag value source for daemon capability; Attached is untagged and imports mode.

Emptying daemon/service and daemon/ui under the tag keeps kardianos/service and fyne.io/systray out of the dependency graph; importing either package in a nodaemon build is a compile error at the first use. Desktop notifications live in sisyphus/desktop (untagged, beeep); omit that import in CLI-only binaries to keep beeep out. The rest of sisyphus/daemon — polling, fan-in, dedupe, cursors, schedules, watermarks, sockets — is untagged and stays available, because none of it requires a service to be running.

Encrypted, key-escrowed backups
ctx := context.Background()
sealed, store, err := sisyphus.Backup(ctx, sisyphus.BackupSpec{
    Files:         []string{cfgDB, dataDB},
    SecretBackend: "auto",       // bw → op → OS keyring
    SecretService: "myapp",      // keyring service name
    SecretName:    "backup-key", // key entry name
})
// ... write `sealed` somewhere ...

names, _, err := sisyphus.Restore(ctx, sisyphus.RestoreSpec{
    Sealed: sealed, SecretBackend: "auto", SecretService: "myapp",
    SecretName: "backup-key", DestDir: home,
})

The AES key is generated on first backup and escrowed in the secret manager; it never travels with the archive, and Backup/Restore are package functions independent of Manager so restore works even when the config DB is corrupt.

KV and journal
ctx := context.Background()
store, _ := kv.Open(ctx, filepath.Join(home, "tokens.duckdb"))
_ = store.Put(ctx, "tokens", "github", jsonBlob, time.Time{}) // zero time = no expiry
entry, ok, _ := store.Get(ctx, "tokens", "github")

log, _ := journal.Open(ctx, filepath.Join(home, "audit.duckdb"))
parent, _ := log.Begin(ctx, "job", "nightly", map[string]string{"env": "prod"})
_, _ = log.Add(ctx, journal.Run{ParentID: parent, Kind: "step", Name: "sync", Count: 3}, records)
_ = log.RollUp(ctx, parent) // roll child counts up into the parent

Development

make build          # go build ./...
make check          # build + fmt-check + lint + govulncheck + test (CI gate is `make ci`)
make test           # go test ./...
make check TAGS=nodaemon   # same gate for the daemon-free configuration

TAGS threads extra build tags through build, vet, lint, and test.

Linters live in the nested tools/ module (go tool -modfile=tools/go.mod) so they stay out of the consumer dependency graph.

Tests run offline. The secret backends shell out to bw/op only when present; their availability probes are stubbable, and the keyring path is tested with go-keyring's mock.

Local multi-repo development (go.work)

When editing sisyphus alongside munin/viewkit, use an uncommitted go.work in the consumer (typically munin) that uses the sibling checkouts. Do not commit go.work / go.work.sum (gitignored here) and do not add committed replace directives — CI and published consumers build against tagged pins.

License

Released under the MIT License.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Backup

func Backup(ctx context.Context, spec BackupSpec) (sealed []byte, storeName string, err error)

func Restore

func Restore(ctx context.Context, spec RestoreSpec) (names []string, storeName string, err error)

Types

type Action

type Action int
const (
	ActionImport Action = iota
	ActionUseFile
	ActionUseDB
)

type BackupSpec

type BackupSpec struct {
	Files         []string
	SecretBackend string
	SecretName    string
	SecretService string
}

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

func Open

func Open(ctx context.Context, home string, opts Options) (*Manager, error)

func (*Manager) Close

func (m *Manager) Close() error

func (*Manager) Current

func (m *Manager) Current(ctx context.Context, name string) (Version, bool, error)

func (*Manager) DB

func (m *Manager) DB() *configdb.Store

func (*Manager) History

func (m *Manager) History(ctx context.Context, name string, limit int) ([]Version, error)

func (*Manager) Home

func (m *Manager) Home() string

func (*Manager) Import

func (m *Manager) Import(ctx context.Context, name string, content []byte, format string) error

func (*Manager) Mode

func (m *Manager) Mode() Mode

func (*Manager) Reconcile

func (m *Manager) Reconcile(ctx context.Context, name string, fileContent []byte, format string, hasFile bool, r Resolver) (content []byte, effFormat string, err error)

type Mode

type Mode int
const (
	ModeBoth Mode = iota
	ModeFileStore
	ModeDuckDB
)

type Options

type Options struct {
	Mode         Mode
	ConfigDBName string
}

type Reconciliation

type Reconciliation struct {
	Name        string
	FileContent []byte
	FileFormat  string
	DB          Version
	HasDB       bool
}

type Resolver

type Resolver interface {
	Resolve(Reconciliation) (Action, error)
}

type RestoreSpec

type RestoreSpec struct {
	Sealed        []byte
	SecretBackend string
	SecretName    string
	SecretService string
	DestDir       string
}

type Version

type Version = configdb.Version

Directories

Path Synopsis
ui
Package desktop sends OS desktop notifications.
Package desktop sends OS desktop notifications.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL