Help us shape the future of Go! Take the 2026 Go Developer Survey

config

package
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package config manages beardrive's global state under the beardrive home directory (default ~/.bdrive, overridable with $BDRIVE_HOME): the device identity and the registry of mounted folders.

Index

Constants

View Source
const DefaultServer = "https://beardrive.ai"

DefaultServer is used wherever a server is needed and none is configured: bare `bdrive login`, and `bdrive init` on a never-logged-in device.

View Source
const ProjectDir = ".bdrive"

ProjectDir is the per-folder settings directory at the mount root. It carries the mount's stable identity, so a project keeps syncing after the folder is renamed or moved — nothing is keyed by the path. It travels with the folder (copy the folder to a new machine and `bdrive init` resumes the same project) but is never synced, and it holds no session credentials — those stay in the bdrive home.

View Source
const WorkspaceFile = "workspace.json"

WorkspaceFile is the manifest at a workspace root, inside the root's .bdrive directory beside — never as — a project's config.json.

The name is the whole guard. DESIGN.md put the manifest IN config.json and told the readers apart by a "kind" field. That is fine for Go and wrong for the agent-hook walk-up, which is pure shell running on every tool call of every agent session on the machine: it would have to inspect each ancestor's config to decide whether to stop there.

Not because it is unaffordable — `read` is a builtin, so a shell-only check costs no processes — but because it makes correctness in that guard depend on knowing what a workspace is. A distinct file name makes the walk-up, IsMount and LoadProject right by construction: nothing has to have heard of workspaces to behave correctly around one.

View Source
const WorkspaceKind = "workspace"

WorkspaceKind is the manifest's self-description, kept for the case the two files ever meet anyway: a hand-written or older root that put the manifest in config.json is refused by LoadProject and is not a mount to IsMount, rather than reading as a project with no identity.

Variables

View Source
var ReservedDirs = map[string]bool{".git": true, ProjectDir: true}

ReservedDirs are directory names BearDrive never syncs, at any depth in a mount: .bdrive is the mount's own identity (syncing it would let one device silently repoint another) and .git carries hook scripts that would run on a teammate's next commit. The rule lives here beside ProjectDir because two packages enforce it — the sync engine on scan and on materialize, the hub on every destination path a client names — and two copies would drift.

Match through ReservedDir, never by indexing this map: the comparison is case-insensitive because BearDrive's primary filesystems (APFS, NTFS) are. An exact-match guard lets ".GIT/hooks/pre-commit" through, and the filesystem then resolves it into the real .git/hooks.

Functions

func AgentConfigDir added in v0.15.0

func AgentConfigDir(name string) bool

AgentConfigDir reports whether a path segment names an agent's configuration directory — the keys of agentHookConfigs — under the same case and trailing-dot folding ReservedDir explains.

It exists for one caller: `bdrive init` refusing such a directory as a MOUNT ROOT. The reserved-path rule only covers segments BELOW a root, so mounting ~/.claude leaves its settings.json a top-level file with no directory segment to match on — along with .credentials.json and every saved session under projects/. Only that direction leaks: a mount that CONTAINS ~/.claude sees .claude/settings.json, reserved at any depth.

Exported here rather than spelled as a literal list in cmd/bdrive for the reason agentHookConfigs' own comment gives: a second copy of that list is how .mcp.json drifted out of it once already.

func AgentHookConfig added in v0.15.0

func AgentHookConfig(p string) bool

AgentHookConfig reports whether a slash-separated path is an agent's project-level hook configuration. See agentHookConfigs.

func CheckRootPlacement added in v0.16.0

func CheckRootPlacement(folder string) error

CheckRootPlacement is checkRootHere plus the rules that require looking at ANCESTORS: roots do not nest, and a root is never inside a project.

IT READS ONE FILE PER ANCESTOR, up to the filesystem root, and those are directories the caller never named — so any one of them being a FIFO, a dead network mount or a TCC-gated folder blocks it forever. That is why DesignateWorkspace does NOT call this: it runs inside the desktop connect, where blocking means a UI stuck at "connecting" with no cancel, no undo, and a 409 on every retry.

SO NOTHING IN THE SHIPPED PRODUCT CALLS THIS. Its only caller is InitWorkspace, which has no production callers either — `bdrive init` never designates a root, it only refuses to mount one. Both ancestor rules are therefore dormant today, enforced by tests and waiting for a gesture that can afford to block ("designate this existing tree"). The connect flow applies checkRootHere and, as DESIGN.md states, does not enforce them.

func DesignateWorkspace added in v0.16.0

func DesignateWorkspace(root string, p WorkspaceProject) (bool, error)

DesignateWorkspace makes root a workspace root holding exactly p, and reports whether it created the manifest (false when root already was one).

Scan-free on purpose, and that is the whole point of it existing beside InitWorkspace: this runs inside the desktop connect flow, where a blocking call is a UI wedged forever with no way back. It writes what the caller already knows — the project it just created — and leaves discovering everything else to the daemon's refresh, which can afford to block.

The caller must already have proven root is reachable (the connect flow stats and writes inside it before reaching here); this adds one stat and one atomic write.

func ForgetMount added in v0.18.0

func ForgetMount(mountID string) error

ForgetMount removes mountID from mounts.json under the registry lock so concurrent enrollments are never overwritten with a stale snapshot.

func HasManifest added in v0.16.0

func HasManifest(folder string) bool

HasManifest reports whether anything at all occupies the manifest path.

A stat, so it cannot block, and that is the point: it answers the question every caller on a critical path actually has — "is this folder already spoken for as a root?" — without opening anything. It is deliberately coarser than IsWorkspaceRoot: a file there that is not a manifest still counts, because the right response to an unknown file at that path is to leave it alone, never to overwrite it.

func Home

func Home() (string, error)

Home returns the beardrive home directory ($BDRIVE_HOME or ~/.bdrive).

func InitWorkspace added in v0.16.0

func InitWorkspace(folder string) error

InitWorkspace designates folder as a workspace root and writes the manifest from a scan of the projects already inside it.

IT SCANS, so it inherits ScanWorkspace's warning: only call it where blocking forever is harmless. It has NO production callers — the connect flow uses DesignateWorkspace, which is scan-free — and it exists for tests and for a future "designate this existing tree" gesture that can afford to block. If you are reaching for it from a command or a request handler, you want DesignateWorkspace.

Placement rules are CheckRootPlacement's, shared with DesignateWorkspace.

func IsMount added in v0.4.0

func IsMount(folder string) bool

IsMount reports whether folder is a BearDrive mount root, i.e. has a .bdrive/config.json — even an unparseable or unreadable one, so callers that must not treat a mount as plain files (e.g. a parent mount's scanner) stay safe.

A workspace root is NOT a mount, and needs nothing here: its manifest has its own name (WorkspaceFile), so a root has no config.json to stat. Reading the file to check its kind was tried and reverted — this runs per directory in the syncer's walk, where one unreadable or wedged config would hang a scan that a stat completes. The agent-hook walk-up matches on the same file name for the same reason, so Go and shell agree: a manifest hand-planted in config.json reads as a mount to both.

func IsWorkspaceRoot added in v0.16.0

func IsWorkspaceRoot(folder string) bool

IsWorkspaceRoot reports whether folder carries a workspace manifest — a file that parses and says so.

IT READS THE FILE, so it can block: a FIFO, a device node or a file on a stalled network mount at that path stops it forever. Use HasManifest where only existence matters, which is everywhere a UI or a command would hang.

func LoadMounts

func LoadMounts() (map[string]MountInfo, error)

LoadMounts returns the mount-ID → mount registry.

func NewMountID

func NewMountID() string

NewMountID mints a stable mount identity.

func RefreshWorkspace added in v0.16.0

func RefreshWorkspace(root string) error

RefreshWorkspace rewrites root's manifest from a scan, and is a no-op unless root already is a workspace root: a manifest is never created by stumbling onto a parent directory, only by the connect flow that deliberately designates one (DesignateWorkspace).

ONLY CALL THIS WHERE BLOCKING IS HARMLESS. ScanWorkspace reads a directory the user chose plus one config per child, none of it bounded — a FIFO, a TCC-gated folder or a dead network path stops it forever. Today its one caller is the goroutine daemon.Run starts, where a stall costs a stale index and nothing else. Putting it in front of a command or a UI step has hung `bdrive init`, `startSync` and the desktop connect, once each.

func RefreshWorkspaceOf added in v0.16.0

func RefreshWorkspaceOf(folder string) error

RefreshWorkspaceOf refreshes the manifest of folder's parent, if the parent is a root. It is what a project-creating flow calls: only the root directly above a new project can gain an entry, since roots do not nest.

func RememberVolumeRemote added in v0.18.0

func RememberVolumeRemote(mountID, remote string) error

RememberVolumeRemote records a mount's remote in an existing volume store. It never creates the volume directory: forgotten legacy stores are stamped only when their data is already present on disk.

func ReservedDir added in v0.15.0

func ReservedDir(name string) bool

ReservedDir reports whether a path segment names a reserved directory, under every spelling a filesystem folds onto the same directory.

Case is one such folding (APFS, NTFS). Trailing dots and spaces are another: NTFS and SMB strip them when opening a path, so ".git./hooks/pre-commit" IS .git/hooks/pre-commit there — the same executable-hook plant an exact-match guard let through as ".GIT".

func ReservedName added in v0.15.0

func ReservedName(name string) bool

ReservedName reports whether a bare file name never syncs. Case-insensitive for the same reason as ReservedDir.

func ReservedPath added in v0.15.0

func ReservedPath(p string) bool

ReservedPath reports whether a slash-separated path is one BearDrive never carries: under a reserved directory, named like one, a reserved file name, or an agent's project-level hook config.

func SameRemote added in v0.18.0

func SameRemote(a, b string) bool

SameRemote reports whether two remote URLs identify the same project.

func SaveMounts

func SaveMounts(m map[string]MountInfo) error

SaveMounts writes the registry, stamping an identity on any row that has none. Every row written before Dev/Ino existed carries zeros, and the move-vs-copy discriminator is only consulted when a row HAS one — so without this backfill the guard is inert on every mount already enrolled on every machine, which is to say on all of them. The stamp is taken while the recorded path still holds this mount, which is the only moment the answer is unambiguous; a row whose path has already stopped answering is left alone rather than given the identity of whatever is sitting there now.

func SaveSettings

func SaveSettings(s Settings) error

SaveSettings persists the device settings.

func SaveWorkspace added in v0.16.0

func SaveWorkspace(root string, w Workspace) error

SaveWorkspace writes the manifest at root.

It will not resurrect a root the user has deleted: MkdirAll happily rebuilds the whole chain, so a refresh racing an `rm -rf` of the root recreated the directory (observed 9 times in 300 runs) — leaving a folder the user removed with a .bdrive in it. The root itself must already exist; only the .bdrive inside it is created here.

func SharedVolumeHasIgnoreState added in v0.18.0

func SharedVolumeHasIgnoreState(remote, excludeMountID string) bool

SharedVolumeHasIgnoreState reports whether a shared project volume currently resolves .bdriveignore to a real file whose blob is present locally.

func SharedVolumeIDForRemote added in v0.18.0

func SharedVolumeIDForRemote(remote, excludeMountID string) string

SharedVolumeIDForRemote finds a local volume already associated with remote.

func UndesignateWorkspace added in v0.16.0

func UndesignateWorkspace(root string) error

UndesignateWorkspace removes root's manifest, un-rooting the folder. Used by the connect flow to hand a folder back when a connect it designated then failed. It removes the manifest and nothing else. Removing the .bdrive directory too was tried: "only if now empty" is also true of an empty .bdrive the user already had, and os.Remove unlinks a symlink whatever it points at. An empty .bdrive left behind is inert — not a mount, not a root — which is a far better outcome than deleting a directory this run did not create.

func ValidMountID added in v0.15.0

func ValidMountID(id string) bool

ValidMountID reports whether id may be used as a mount identity.

func VolumeDir

func VolumeDir(mountID string) (string, error)

VolumeDir returns the local store dir of a mount, keyed by its stable mount ID (never the folder path — that's what makes renames/moves free).

The id is validated here, where it becomes a path: LoadProject checks the one it reads out of a folder's config, but mounts.json is unmarshalled into a map whose KEYS nothing looks at, and `bdrive resume` — what the login agent runs at every boot — builds this path out of the key. A registry entry is plain JSON in $BDRIVE_HOME that anything running as the user can write.

Types

type Device

type Device struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Author string `json:"author"`
}

Device identifies this machine and its operator in journals.

func LoadDevice

func LoadDevice() (Device, error)

LoadDevice loads the device identity, creating one on first use.

func LoadDeviceSeeded added in v0.16.0

func LoadDeviceSeeded(seed string) (Device, error)

LoadDeviceSeeded is LoadDevice for a process whose $BDRIVE_HOME may not

survive a restart, which is every hub on an ephemeral filesystem.

An identity already on disk still wins — a self-hosted hub with a real home
keeps the id every peer already has in its journal listing. What changes is
the OTHER branch: instead of minting at random, the id is derived from a
seed that identifies this installation (its storage root), so the same hub
over the same store is the same writer however many times it is restarted.

This is not a nicety. The hub journals its own writes under this id, so a
random one per boot means a permanent journal/<rand>.jsonl per boot per
project — 43 of them on one production project with five real devices, a
long tail at 321 bytes apiece. A journal is append-only and may hold real
work, so nothing can ever collect them, and every reader's cold fold pays a
round trip for each.

The seed must identify the STORE, not the process: two hubs on one store
sharing an id would both append one journal key, which is the corruption
"each device writes only its own journal" exists to prevent. That is also
why this stays a single derived id rather than one per instance — the hub
is pinned to one instance until per-instance identities exist
(docs/hub-load-prd.md Phase 3).

type MountInfo

type MountInfo struct {
	Path   string `json:"path"`
	Volume string `json:"volume,omitempty"`
	Remote string `json:"remote,omitempty"`
	// VolumeID is the shared volume store directory name under ~/.bdrive/volumes/
	// when multiple mounts (or a re-created worktree) connect to the same remote
	// project. Empty means VolumeDir uses the mount's own ID (the legacy and
	// first-mount default).
	VolumeID string `json:"volume_id,omitempty"`
	// Dev+Ino are the filesystem's identity for Path at the time this row was
	// written. They are what tells a MOVED folder (rename: same inode, new
	// path) from a second folder carrying a copy of the same
	// .bdrive/config.json (new inode). Zero on rows written before this field
	// existed, and on platforms with no such identity; see dirID.
	Dev uint64 `json:"dev,omitempty"`
	Ino uint64 `json:"ino,omitempty"`
}

MountInfo is the registry's view of one mount: where the folder currently lives. The source of truth for identity/settings is the folder's own .bdrive/config.json; the registry only remembers the last-known path (for `bdrive status` and the daemon) and self-heals when the folder moves.

type Project

type Project struct {
	// ID is the stable mount identity (m-xxxxxxxx). The volume store, the
	// daemon, and the registry are keyed by it, never by the folder path.
	ID     string `json:"id"`
	Volume string `json:"volume,omitempty"`
	Remote string `json:"remote,omitempty"`
	// Include optionally narrows what syncs: when non-empty, only paths
	// matching one of these patterns (gitignore-style, same syntax as
	// .bdriveignore) are scanned and materialized.
	Include []string `json:"include,omitempty"`
	// PostSync is a shell command run on THIS device after a cycle applies a
	// teammate's changes, with the applied batch as JSON on stdin — the event
	// a local index, cache or notifier can hang off instead of polling.
	//
	// It lives here, and only here, on purpose: .bdrive is in ReservedDirs and
	// never syncs, so no hub response and no peer's journal can put a command
	// on someone else's machine.
	PostSync string `json:"post_sync,omitempty"`
}

Project holds the settings stored in <folder>/.bdrive/config.json.

func EnrollMount added in v0.15.0

func EnrollMount(folder string) (Project, bool, error)

EnrollMount is ResolveMount plus the one thing ResolveMount refuses to do: create this device's registry row for a project. It is the enrollment gesture, so exactly one caller has any business using it — `bdrive init` (startSync).

func LoadProject

func LoadProject(folder string) (Project, bool, error)

LoadProject reads <folder>/.bdrive/config.json; ok is false if it does not exist.

func ResolveMount

func ResolveMount(folder string) (Project, bool, error)

ResolveMount loads a folder's project settings and self-heals the registry: if the folder was renamed or moved, the registry entry is updated to the new path so `bdrive status` and the daemon find it again.

It never CREATES a row. Enrolling this device in a project is `bdrive init` (EnrollMount) and nothing else: .bdrive/config.json travels with a folder — a clone, an unpacked archive, a colleague's copy — so its presence is not consent to sync, and syncBlocked's "init" arm is the gate that says so. Creating the row here made that gate unreachable for every command that resolves a folder before consulting it (`bdrive restore`, `bdrive forget`): one run inside an attacker-supplied folder put an attacker-chosen remote in the registry, and the login autostart runs `bdrive resume`, which starts a daemon for every enrolled row. A function every folder-taking command calls must not be a write with a read-shaped name.

func SaveProject

func SaveProject(folder string, p Project) (Project, error)

SaveProject writes <folder>/.bdrive/config.json, assigning a mount ID on first save.

type Settings

type Settings struct {
	// Server is the default bdrive web server for `bdrive init`.
	Server string `json:"server,omitempty"`
	// Token authenticates this device to Server (minted by `bdrive login`).
	// Sent as a Bearer header.
	Token string `json:"token,omitempty"`
	// Email and Name identify the signed-in account; journal ops carry them
	// so history shows who changed what.
	Email string `json:"email,omitempty"`
	Name  string `json:"name,omitempty"`
}

Settings are device-wide defaults, stored at $BDRIVE_HOME/settings.json.

func LoadSettings

func LoadSettings() (Settings, error)

LoadSettings reads the device settings; a missing file is zero settings.

type Workspace added in v0.16.0

type Workspace struct {
	Kind     string             `json:"kind"`
	Projects []WorkspaceProject `json:"projects"`
}

Workspace is the manifest at a workspace root — the one place that answers "what on this machine is BearDrive, and what isn't".

It is an INDEX, not the source of truth. Identity stays in each project folder's own .bdrive/config.json, which is what keeps a folder meaningful on its own: copy it to another machine and `bdrive init` resumes the same project. Move identity up here and a folder alone means nothing.

So nothing is ever resolved FROM this file — no volume path, no journal, no mount id, no permission. On disagreement the folder wins: the manifest is rebuilt from a scan (ScanWorkspace) and a stale or hand-edited entry is corrected, never obeyed.

func LoadWorkspace added in v0.16.0

func LoadWorkspace(root string) (Workspace, bool, error)

LoadWorkspace reads <root>/.bdrive/workspace.json; ok is false if there is none. A file that does not describe itself as a workspace is an error, not an empty manifest.

func ScanWorkspace added in v0.16.0

func ScanWorkspace(root string) (Workspace, error)

ScanWorkspace builds the manifest from what is on disk: the root's immediate children that are mounts, in name order (os.ReadDir sorts).

Immediate children only, and deliberately so. Nested mounts DO exist in this product (see syncer's vNested handling) — the manifest simply does not index them: it answers "which folders in my workspace root are projects", and a project inside a project is that project's business.

This is the only way a manifest is ever produced, which is what makes the folder the source of truth.

type WorkspaceProject added in v0.16.0

type WorkspaceProject struct {
	Path string `json:"path"`
	ID   string `json:"id"`
}

WorkspaceProject is one entry of the index: where a project folder sits (relative to the root, so the root can be renamed or moved) and which mount it is.

Jump to

Keyboard shortcuts

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