sisyphus

package module
v0.2.5 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 9 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) ConfigStore 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/duckfile Plugin-owned DuckDB files + ad-hoc 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/stream Event pipelines: Poll/Source, fan-in, Subject, dedupe, KV-backed Cursor/Watermark.
sisyphus/ipc Local transport: unix sockets / named pipes (Listen/Dial/Broadcast/IsListening).
sisyphus/schedule Periodic jobs: Run drives Jobs with backoff; RunAt for one-shots.
sisyphus/tray Daemon run-state (State) + icon assets (untagged) and the systray (!nodaemon).
sisyphus/daemon Residue: SignalContext and the capability probe Attached.
sisyphus/daemon/service OS service install/start/stop wrapper. 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.Open(backend, service) (defaults to "sisyphus"); backup threads it via BackupSpec.Secret.Service.
  • Backup file list + secret name — supplied on BackupSpec / RestoreSpec.

Usage

Config reconciliation

ConfigStore makes DuckDB the source of truth for file-backed config, and never auto-imports — you decide via Plan/Apply when file and DB disagree.

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

raw, format, _ := config.ReadFile(home)
item := sisyphus.Item{Name: "config", FileContent: raw, FileFormat: format}
drifted, _ := m.Plan(ctx, item)
for _, rec := range drifted {
    _, _, _ = m.Apply(ctx, rec, decide(rec)) // your Action per document
}
content, format, err := m.Effective(ctx, item)
// then: config.ParseInto(&myCfg, content, format, "MYAPP_")

Plan returns one Reconciliation per document that has drifted; you resolve each with Apply and an Action (ActionImport / ActionUseFile / ActionUseDB), then read the result with Effective. ConfigStore.Current/Import/History/Forget cover the common config-DB operations.

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.

UnauthorizedPolicy is not a global "require authentication" switch. It affects only an unauthorized CLI user: when CLIUnauthorized returns an error, PolicyBlock propagates that error and blocks the command; PolicyWarn (the zero value) discards it and allows the command to continue.

Mode and state Gate behavior
CLI, unauthenticated Runs CLIUnauthenticated; any error blocks.
CLI, unauthorized, PolicyWarn (default) Runs CLIUnauthorized, discards its error, and continues.
CLI, unauthorized, PolicyBlock 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")
    },
    UnauthorizedPolicy: mode.PolicyBlock,
})
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, tray.Tray Full API Empty package / compiled out
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 ipc.IsListening — it returns false when !mode.DaemonSupported, otherwise delegates to ipc.IsListening. Gate optional UI and features on Attached, and use ipc.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 compiling out tray.Tray under the tag keeps kardianos/service and fyne.io/systray out of the dependency graph; using either 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. stream, ipc, schedule, and the tray state half — polling, fan-in, dedupe, cursors, schedules, watermarks, sockets, run-state icons — are untagged and stay 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},
    Secret: sisyphus.SecretRef{
        Backend: "auto",       // bw → op → OS keyring
        Service: "myapp",      // keyring service name
        Name:    "backup-key", // key entry name
    },
})
// ... write `sealed` somewhere ...

names, _, err := sisyphus.Restore(ctx, sisyphus.RestoreSpec{
    Sealed:  sealed,
    Secret:  sisyphus.SecretRef{Backend: "auto", Service: "myapp", Name: "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 ConfigStore 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.StartRun(ctx, "job", "nightly", map[string]string{"env": "prod"})
_, _ = log.Add(ctx, journal.Run{ParentID: parent, Kind: "step", Name: "sync", Count: 3}, records)
_ = log.FinishRun(ctx, parent) // stamp finished; 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

Overview

Package sisyphus is a small, app-agnostic toolkit for the plumbing every CLI or service ends up rewriting: config reconciliation, DuckDB-backed stores, secret escrow, encrypted backups, and daemon primitives.

The root package carries the two application-facing facades:

  • ConfigStore reconciles named config documents between files on disk and a versioned DuckDB store (see Open, Plan, Apply, Effective).
  • Backup and Restore produce and consume encrypted tar archives whose AES key is escrowed in a secret manager (see BackupSpec, RestoreSpec).

Everything application-specific — home directory, file names, namespaces, key names — is passed in by the caller; nothing is baked in. Each sub-package (config, configdb, kv, journal, secret, sealed, backup, daemon, mode, lifecycle, redact, duckfile, ...) is usable on its own.

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)

Backup archives spec.Files into a tar, encrypts it with AES-256-GCM, and returns the sealed bytes plus the name of the secret store that holds the key. The key is created and escrowed on first use (under spec.Secret.Name, "backup-key" when empty) and never travels with the archive. Backup is independent of ConfigStore, so it works without opening the config DB.

func Restore

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

Restore decrypts spec.Sealed with the escrowed key and swaps the archived files into spec.DestDir, returning the restored basenames plus the name of the secret store that supplied the key. A missing key is an error — restore never generates one. Like Backup it is independent of ConfigStore, so a corrupt config DB does not block restoring it.

Types

type Action

type Action int

Action is the caller's decision for one drifted document, passed to Apply.

const (
	// ActionImport stores the file content as the new DB snapshot and uses it.
	ActionImport Action = iota
	// ActionUseFile uses the file content without touching the DB.
	ActionUseFile
	// ActionUseDB uses the stored DB snapshot, ignoring the file.
	ActionUseDB
)

type Backend added in v0.2.0

type Backend int

Backend selects where a ConfigStore keeps config documents.

const (
	// BackendBoth keeps files and the DB reconciled against each other.
	BackendBoth Backend = iota
	// BackendFiles reads files only; no database is opened.
	BackendFiles
	// BackendDB reads the database only.
	BackendDB
)

type BackupSpec

type BackupSpec struct {
	Files  []string
	Secret SecretRef
}

BackupSpec describes one encrypted backup: which Files to archive and which Secret entry escrows the encryption key.

type ConfigStore added in v0.2.0

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

ConfigStore is the facade over an application's config home: named config documents on disk, versioned snapshots in a DuckDB store, or both, reconciled through Plan/Apply/Effective.

A ConfigStore opened with BackendFiles carries no database: reads report absent with a nil error, Import and Forget return configdb.ErrUnavailable, and Close returns nil (as it does on a nil *ConfigStore).

func Open

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

Open returns a ConfigStore rooted at home. Unless opts.Backend is BackendFiles it opens (creating if needed) the config database inside home, named by opts.ConfigDBName ("config.duckdb" when empty).

func (*ConfigStore) Apply added in v0.2.0

func (m *ConfigStore) Apply(ctx context.Context, rec Reconciliation, act Action) (content []byte, format config.Format, err error)

Apply resolves one planned Reconciliation with the caller's decision and returns the effective content and format.

func (*ConfigStore) Backend added in v0.2.0

func (m *ConfigStore) Backend() Backend

Backend reports which backend this store was opened with.

func (*ConfigStore) Close added in v0.2.0

func (m *ConfigStore) Close() error

Close releases the underlying config database. It is safe on a nil *ConfigStore and on one opened with BackendFiles; both return nil.

func (*ConfigStore) Current added in v0.2.0

func (m *ConfigStore) Current(ctx context.Context, name string) (Snapshot, bool, error)

Current returns the stored current snapshot of the named document. Without a database (BackendFiles) it reports absent with a nil error.

func (*ConfigStore) Effective added in v0.2.0

func (m *ConfigStore) Effective(ctx context.Context, it Item) (content []byte, format config.Format, err error)

Effective resolves the content for an item that needs no reconciliation: BackendFiles uses the file, BackendDB uses the stored snapshot, and BackendBoth uses the file when present, falling back to the stored snapshot.

func (*ConfigStore) Forget added in v0.2.0

func (m *ConfigStore) Forget(ctx context.Context, name string) error

Forget drops a document's current snapshot and history from the store.

func (*ConfigStore) Generation added in v0.2.0

func (m *ConfigStore) Generation() (string, bool)

Generation reports the store's change marker: an opaque value that changes with every committed write, so pollers can detect change without opening the database.

func (*ConfigStore) History added in v0.2.0

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

History returns up to limit archived snapshots of the named document, newest first (limit <= 0 means 50). Without a database (BackendFiles) it returns nil with a nil error.

func (*ConfigStore) Home added in v0.2.0

func (m *ConfigStore) Home() string

Home returns the home directory the store was opened at.

func (*ConfigStore) Import added in v0.2.0

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

Import stores content as the named document's new current snapshot, archiving the previous one into its history. Without a database (BackendFiles) it returns configdb.ErrUnavailable.

func (*ConfigStore) Plan added in v0.2.0

func (m *ConfigStore) Plan(ctx context.Context, items ...Item) ([]Reconciliation, error)

Plan returns one Reconciliation per item that has drifted: the file differs from the DB snapshot, or the item exists on only one side. Items in sync (or absent on both sides) are omitted. In BackendFiles and BackendDB it returns nil: there is nothing to reconcile, and Effective resolves content per the backend.

type Item added in v0.2.0

type Item struct {
	Name        string
	FileContent []byte // empty = no file on disk
	FileFormat  config.Format
}

Item is one named config document to reconcile against the DB.

type Options

type Options struct {
	// Backend selects files, DB, or both (the zero value, BackendBoth).
	Backend Backend
	// ConfigDBName is the config database filename inside home. Empty means
	// "config.duckdb".
	ConfigDBName string
}

Options configures Open.

type Reconciliation

type Reconciliation struct {
	Name        string
	FileContent []byte
	FileFormat  config.Format
	DB          Snapshot
}

Reconciliation is one drifted document reported by Plan: the file-side content next to the stored DB snapshot (zero when only one side exists), awaiting a caller decision via Apply.

func (Reconciliation) HasDB

func (r Reconciliation) HasDB() bool

HasDB reports whether a stored snapshot exists for this item. A zero DB Snapshot expresses absence; stored snapshots always carry a non-empty hash.

func (Reconciliation) HasFile added in v0.2.0

func (r Reconciliation) HasFile() bool

HasFile reports whether a file version exists for this item.

type RestoreSpec

type RestoreSpec struct {
	Sealed  []byte
	Secret  SecretRef
	DestDir string
}

RestoreSpec describes one restore: the Sealed archive bytes, the Secret entry holding its key, and the DestDir the files are written into.

type SecretRef added in v0.2.0

type SecretRef struct {
	Backend string
	Name    string
	Service string
}

SecretRef selects the secret-manager entry holding a backup key: which Backend to use (any spelling secret.ParseBackend accepts), the entry Name (defaulting to "backup-key"), and the Service namespace for keyring-style backends.

type Snapshot added in v0.2.0

type Snapshot = configdb.Snapshot

Snapshot is one stored content snapshot of a named config document.

Directories

Path Synopsis
Package auth implements the credential-acquisition flows an application wires its own OAuth endpoints into: an authorization-code loopback server (LoopbackAuthCode), the device-authorization grant (DeviceToken), and a small helper for shelling out to auth CLIs (RunTool).
Package auth implements the credential-acquisition flows an application wires its own OAuth endpoints into: an authorization-code loopback server (LoopbackAuthCode), the device-authorization grant (DeviceToken), and a small helper for shelling out to auth CLIs (RunTool).
Package backup snapshots a set of files into a tar archive and restores such archives atomically.
Package backup snapshots a set of files into a tar archive and restores such archives atomically.
Package config resolves an application's home directory, reads its config file, and parses YAML or JSON into the caller's own struct with environment-variable overrides layered on top.
Package config resolves an application's home directory, reads its config file, and parses YAML or JSON into the caller's own struct with environment-variable overrides layered on top.
Package configdb is a versioned, name-keyed config-document store in a single DuckDB file: one current snapshot per name (store_current) plus its archived predecessors (store_history).
Package configdb is a versioned, name-keyed config-document store in a single DuckDB file: one current snapshot per name (store_current) plus its archived predecessors (store_history).
Package daemon holds what is genuinely daemon-flavored: SignalContext for shutdown-signal handling and Attached, the capability-aware probe for a running service.
Package daemon holds what is genuinely daemon-flavored: SignalContext for shutdown-signal handling and Attached, the capability-aware probe for a running service.
service
Package service wraps kardianos/service to install, start, stop and run a program as an OS service (systemd, launchd, Windows SCM, ...), system-wide or per-user.
Package service wraps kardianos/service to install, start, stop and run a program as an OS service (systemd, launchd, Windows SCM, ...), system-wide or per-user.
Package desktop sends OS desktop notifications.
Package desktop sends OS desktop notifications.
Package duckfile owns plugin-scoped DuckDB files: each plugin opens its own database with its own schema and queries it through string tables.
Package duckfile owns plugin-scoped DuckDB files: each plugin opens its own database with its own schema and queries it through string tables.
Package duckopt carries the DuckDB handle tuning shared by every sisyphus store package (kv, configdb, journal, duckfile).
Package duckopt carries the DuckDB handle tuning shared by every sisyphus store package (kv, configdb, journal, duckfile).
internal
crypt
Package crypt is the AES-256-GCM sealing shared by the backup archive format and the sealed store.
Package crypt is the AES-256-GCM sealing shared by the backup archive format and the sealed store.
duckdb
Package duckdb is the shared DuckDB plumbing under every sisyphus store: opening files owner-only (database and WAL alike), the Handle that coordinates single-writer access across processes via sidecar lock files, and small SQL helpers (NULL adapters, QueryTable).
Package duckdb is the shared DuckDB plumbing under every sisyphus store: opening files owner-only (database and WAL alike), the Handle that coordinates single-writer access across processes via sidecar lock files, and small SQL helpers (NULL adapters, QueryTable).
fsutil
Package fsutil holds the generic filesystem helpers behind the config package.
Package fsutil holds the generic filesystem helpers behind the config package.
Package ipc carries events between local processes over unix sockets (named pipes on Windows): Listen/Dial establish the transport, Broadcast fans a stream.Subject out to every connected peer, and IsListening probes for a live listener.
Package ipc carries events between local processes over unix sockets (named pipes on Windows): Listen/Dial establish the transport, Broadcast fans a stream.Subject out to every connected peer, and IsListening probes for a live listener.
Package journal is a generic activity log in a single DuckDB file: runs (optionally nested one level, parent/child) and per-run records, each carrying a free-form string attribute map.
Package journal is a generic activity log in a single DuckDB file: runs (optionally nested one level, parent/child) and per-run records, each carrying a free-form string attribute map.
Package kv is a generic namespaced key/value store in a single DuckDB file, with an optional expiry per entry.
Package kv is a generic namespaced key/value store in a single DuckDB file, with an optional expiry per entry.
Package lifecycle holds home-directory install, clean and nuke primitives, plus a small shell-hook runner (Scripts / Select / Run*) for the bash or PowerShell snippets an application lets its users attach to those moments.
Package lifecycle holds home-directory install, clean and nuke primitives, plus a small shell-hook runner (Scripts / Select / Run*) for the bash or PowerShell snippets an application lets its users attach to those moments.
Package mode names an application's operating surfaces (CLI, serve, daemon, deck, or app-defined) and runs the application's own authorization policy at the right moment through Gate.
Package mode names an application's operating surfaces (CLI, serve, daemon, deck, or app-defined) and runs the application's own authorization policy at the right moment through Gate.
Package redact masks secret-looking values in config content before it is displayed or logged.
Package redact masks secret-looking values in config content before it is displayed or logged.
Package schedule drives periodic jobs: Run polls each Job's Next for its Due time on a fixed tick and executes it with per-job failure backoff; RunAt runs a single function at an absolute time.
Package schedule drives periodic jobs: Run polls each Job's Next for its Due time on a fixed tick and executes it with per-job failure backoff; RunAt runs a single function at an absolute time.
Package sealed is an encrypted credential store: Entry values are AES-256-GCM sealed and kept in a kv.Store, with the encryption key escrowed in the OS keyring (or supplied by the caller).
Package sealed is an encrypted credential store: Entry values are AES-256-GCM sealed and kept in a kv.Store, with the encryption key escrowed in the OS keyring (or supplied by the caller).
Package secret escrows small string secrets (typically encryption keys) in an external secret manager: the Bitwarden CLI (bw), the 1Password CLI (op), or the OS keyring.
Package secret escrows small string secrets (typically encryption keys) in an external secret manager: the Bitwarden CLI (bw), the 1Password CLI (op), or the OS keyring.
Package storeerr holds the sentinel error shared by every sisyphus store package.
Package storeerr holds the sentinel error shared by every sisyphus store package.
Package stream provides event pipelines with resumable position state: polling sources (Poll, PollAdaptive, Source), fan-in and pub/sub plumbing (FanIn, Subject), duplicate suppression across restarts (Deduper), and the small KV-backed persistence primitives that make resumption work (Cursor, Watermark, ScopedKV over the KV interface).
Package stream provides event pipelines with resumable position state: polling sources (Poll, PollAdaptive, Source), fan-in and pub/sub plumbing (FanIn, Subject), duplicate suppression across restarts (Deduper), and the small KV-backed persistence primitives that make resumption work (Cursor, Watermark, ScopedKV over the KV interface).
Package tabular holds the string-table result type shared by the sisyphus query surfaces (journal.Query, duckfile.Query), plus typed accessors for reading cells back out of it.
Package tabular holds the string-table result type shared by the sisyphus query surfaces (journal.Query, duckfile.Query), plus typed accessors for reading cells back out of it.
Package tray models the coarse run-state a long-running process surfaces to the user — State and its icon Assets — and, in non-nodaemon builds, shows it as a system tray icon (Tray).
Package tray models the coarse run-state a long-running process surfaces to the user — State and its icon Assets — and, in non-nodaemon builds, shows it as a system tray icon (Tray).

Jump to

Keyboard shortcuts

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