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

vault

package
v0.10.18 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package vault provides vault scanning and configuration loading.

Index

Constants

View Source
const ConfigRelPath = ".vaultmind/config.yaml"

ConfigRelPath is the vault's type registry, relative to the vault root, in slash form. Its presence is what makes a directory a vault.

The marker used to be the .vaultmind/ directory alone, which is wrong in the one place it matters most: the model cache lives at ~/.vaultmind/models, so on any machine that has downloaded BGE-M3 weights a bare-directory test answers "yes" for the home directory. A registry is written by `init` (or by `index` when a vault is explicitly named); a cache never carries one.

View Source
const GapType = "gap"

GapType is the built-in note type for a question the vault did not answer. index.TypeGap is the same string. The packages cannot import each other; TestGapType_MatchesTheIndexConstant locks them.

View Source
const ImportFolder = "imported"

ImportFolder is the vault folder the import command writes into. importdocs.ImportedDir is this string. propose reads it here: the business layer cannot import importdocs, and the folder name has one source (Principle 7).

View Source
const ImportNoteType = "reference"

ImportNoteType is the type the import command assigns. A new draft does not inherit it. importdocs.NoteType is this string.

View Source
const NoteExtension = ".md"

NoteExtension is the extension of a note file; only these are indexed.

View Source
const OverviewType = "overview"

OverviewType is the built-in note type for a folder overview.

Variables

View Source
var ErrEscapesVault = errors.New("path escapes vault")

ErrEscapesVault reports a path that resolves outside the vault root.

A sentinel rather than a typed error because the callers have incompatible contracts: plan and mutation must answer with their own `path_traversal` error codes, index and cmdutil return plain errors, and a shared error type would force one of them to leak the other's vocabulary. errors.Is is the whole interface.

Functions

func CanonicalKey added in v0.10.13

func CanonicalKey(vaultPath string) string

CanonicalKey is the stable identity of a vault directory: absolute, and symlink-resolved when the path exists. Calibration stores this string on both the write side and the read side, and the possible-gap log hashes it for a per-vault file name. There is no separate vault id.

The default vault path is "." (relative). Without this, two vaults indexed from different working directories would both store "." and collide. EvalSymlinks fails for a path that does not exist yet; Abs still yields a deterministic key.

func EnsureGitignore added in v0.10.17

func EnsureGitignore(vaultRoot string) error

EnsureGitignore creates the vault's state directory and its default ignore policy. An existing policy belongs to the user and is never overwritten.

func Excluded added in v0.9.17

func Excluded(relPath string, excludes []string) bool

Excluded reports whether Scan would pass over the note at the vault-relative relPath under excludes: the file itself is excluded, or a folder above it is.

func ResolveInside added in v0.6.0

func ResolveInside(vaultRoot, rel string) (string, error)

ResolveInside joins rel onto vaultRoot and returns the cleaned absolute path, or ErrEscapesVault if the result would land outside the vault.

This is the CONFINEMENT check, and it is deliberately not the symlink check (see SkipSymlink). They answer different questions and neither implies the other: `notes.md -> ~/.zshrc` passes this function — notes.md really is inside the vault — and writing to it still lands outside. Anything that opens a path built from vault content or vault config wants both.

What it catches is `..` traversal. `filepath.Join` re-roots an absolute second argument inside the first (Join("/v", "/etc/passwd") is "/v/etc/passwd"), so an absolute rel is contained rather than escaping — but it is also silently not the path the operator wrote, which is its own problem; reject those at the point they are configured, not here.

The vault root itself is allowed: some callers resolve "." to mean the root.

vaultRoot is resolved FIRST, to the real directory when the operator named the vault through a symlink (ResolveRoot) and otherwise to an absolute path. The containment test is a string-prefix comparison, and between two relative paths that comparison is simply wrong: filepath.Join collapses the leading "./" the prefix depends on. The same root is what Scan walks, so a path confined here is a path inside the directory that was indexed.

ResolveInside(".", ".vaultmind/index.db")
  cleanVault = "."
  cleanAbs   = ".vaultmind/index.db"        <- Join dropped the "./"
  HasPrefix(".vaultmind/index.db", "./")    <- false, so: refused

A path plainly inside the vault read as an escape. Live effect: every command failed when the vault was named as `.`, while OMITTING the flag — which means the same directory — worked, because that path reaches here already absolute.

This is not a loosening. Resolving first makes the prefix test meaningful rather than accidental, and `..` traversal out of a relative root was refused before and still is (see TestResolveInside_RelativeRoot). It also makes the function keep the promise in its own first line, which said "absolute" while returning a relative path for a relative root — and every caller names the result absPath or dbPath.

func ResolveRoot added in v0.10.11

func ResolveRoot(vaultRoot string) (string, error)

ResolveRoot returns the vault directory the operator named.

filepath.WalkDir lstats its root and does not descend when that root is a symlink: the link is not a directory, so the walk yields one entry and stops. A vault addressed through a stable pointer (vaultmind-identity -> the real directory) then looks empty, and an indexer that treats missing files as deleted wipes the index.

Only that named path is resolved, and only when its final element is a symlink. Links inside the vault stay links — SkipSymlink is the rule for those, and resolving them is the read/write hole it closes. A directory that is not itself a link is returned from Abs unchanged, so an ancestor such as /var -> /private/var does not rewrite every vault path. A path that does not exist yet has nothing to resolve; callers that confine before the directory exists get the absolute path.

One function, used by the walk and by confinement, so both agree on the root.

func SkipSymlink(root, path string, d fs.DirEntry) (rel string, skip bool)

SkipSymlink reports whether a walked directory entry must not be followed.

Every walk over vault content needs this, and each one that skipped it was a hole. filepath.WalkDir does not descend into directory symlinks, but it DOES hand back a file symlink named *.md, and the stdlib calls that take a path follow it:

  • os.ReadFile turns an untrusted vault into a READ primitive — `secrets.md -> ~/.ssh/id_rsa` was hashed, parsed, stored in FTS, embedded, and returned by `ask`.
  • os.WriteFile turns it into a WRITE primitive — `notes.md -> ~/.zshrc` was rewritten in place by the wikilink healer.

The rule is the LINK, not the destination: an entry is skipped whatever it points at, including a target inside the vault. Resolving first and confining after would need EvalSymlinks, whose answer can change between the check and the call, and would let one file be handled under two paths — the duplicate-id class that took three fixes to close.

This is NOT the confinement check. "Stays under the vault root" and "is not a link" are different predicates, and passing the first says nothing about the second: the mutator confines every path it writes and would still have overwritten ~/.zshrc, because notes.md really is inside the vault. Keep them separate; merging them is how one of the two quietly disappears.

d.Type() comes from Lstat, so it describes the link itself rather than its target — no extra syscall, and no window between deciding and acting.

Callers must REPORT what they skip, on their result rather than in a log line: from the outside, a file passed over and a file that was never there look identical. That is why the vault-relative path comes back from this call rather than being each caller's job — four hand-rolled copies of the same filepath.Rel fallback is how they drift.

func ValidSegment added in v0.6.0

func ValidSegment(s string) bool

ValidSegment reports whether s is safe to use as a SINGLE path segment — letters, digits, underscore, dash, and nothing else.

For values that name a directory or file component rather than a path: a note's `type` from its own frontmatter, a section key from its markers. Those are attacker-controlled in any vault you did not write yourself, and they get joined into a path. ResolveInside would catch the resulting escape, but a whitelist is the better answer one level up: a type called "../../../etc" is not a legal type in the first place, whatever it resolves to, and rejecting it names the real problem instead of a path error.

Types

type Config

type Config struct {
	Vault  VaultConfig        `yaml:"vault"`
	Types  map[string]TypeDef `yaml:"types"`
	Git    GitPolicyConfig    `yaml:"git"`
	Index  IndexConfig        `yaml:"index"`
	Memory MemoryConfig       `yaml:"memory"`
	Schema SchemaConfig       `yaml:"schema"`
}

Config represents the full .vaultmind/config.yaml

func LoadConfig

func LoadConfig(vaultRoot string) (*Config, error)

LoadConfig reads .vaultmind/config.yaml from the vault root. Returns defaults if the config file doesn't exist.

func (*Config) IsAuthoritative added in v0.8.0

func (c *Config) IsAuthoritative(typeName string) bool

IsAuthoritative reports whether a type may be cited as evidence. An undeclared type is authoritative, so existing vaults are unaffected.

type GitPolicyConfig

type GitPolicyConfig struct {
	Policy map[string]string `yaml:"policy"`
}

GitPolicyConfig holds git policy overrides.

type IndexConfig

type IndexConfig struct {
	DBPath string `yaml:"db_path"`
}

IndexConfig holds indexing settings.

type MemoryConfig

type MemoryConfig struct {
	AliasMinLength           int     `yaml:"alias_min_length"`
	TagOverlapThreshold      float64 `yaml:"tag_overlap_threshold"`
	ContextPackDefaultBudget int     `yaml:"context_pack_default_budget"`
}

MemoryConfig holds memory engine settings.

type ScanResult added in v0.6.0

type ScanResult struct {
	Files []ScannedFile

	// SkippedSymlinks holds vault-relative paths of *.md symlinks that were not
	// followed. Callers are expected to REPORT these, not drop them: from the
	// outside, a skipped note and a missing note look identical.
	SkippedSymlinks []string

	// Entries is how many filesystem entries the walk visited under the root.
	// The root itself is not one of them. Excluded files and directories count:
	// seeing them is evidence the vault is there. A skipped directory counts
	// once; the walk does not visit what is inside it.
	Entries int

	// Root is the path the walk used, after ResolveRoot.
	Root string

	// Walkable is true when Root is a real directory: it exists, it is not a
	// symlink (a broken one included), and it is a directory.
	Walkable bool
}

ScanResult is what a walk found: the notes to index, and what was passed over for a reason the caller has to answer for.

A struct rather than extra return values because "skipped, and why" is a growing category — and a plain []ScannedFile let a whole class of skip go unmentioned, which is how a note vanishes from an index in silence.

func Scan

func Scan(vaultRoot string, excludes []string) (ScanResult, error)

Scan walks the vault directory and returns all .md files, excluding directories that match any of the exclude patterns.

The root the operator named is resolved first (ResolveRoot). WalkDir does not descend into a root that is a symlink, and an empty result is what made index delete every note. Symlinks inside the vault are never followed — see the guard below for why.

func (ScanResult) SawNothing added in v0.10.11

func (s ScanResult) SawNothing() bool

SawNothing is the one answer the wipe guard and doctor share (Principle 7). True when the root is not a directory the walk can enter, or the walk visited nothing under it. A walk that saw entries — a README, .trash, episodes, excluded or not — saw a vault; zero indexable notes is then a real purge. The zero value is SawNothing, so an unset result cannot authorize a wipe.

type ScannedFile

type ScannedFile struct {
	RelPath string    // Vault-relative path (e.g., "concepts/act-r.md")
	AbsPath string    // Absolute filesystem path
	ModTime time.Time // Last modification time
}

ScannedFile represents a discovered .md file in the vault.

type SchemaConfig

type SchemaConfig struct {
	Aliases map[string][]string `yaml:"aliases"`
}

SchemaConfig holds per-vault schema settings beyond the type registry. Aliases let migrating users keep their existing frontmatter field names (e.g. `last_updated`) while vaultmind validates against canonical names (e.g. `updated`). The map is canonical → list of aliases. Aliasing is non-destructive: vaultmind never rewrites frontmatter to normalize field names; the alias and the canonical are equivalent at validation only.

type TypeDef

type TypeDef struct {
	Required []string `yaml:"required" json:"required"`
	Optional []string `yaml:"optional" json:"optional"`
	Statuses []string `yaml:"statuses" json:"statuses"`
	Template string   `yaml:"template" json:"template"`

	// Authoritative says whether notes of this type may be CITED as evidence
	// via source_ids.
	//
	// Validation only ever checked that a cited note EXISTS, so a curated arc
	// could cite an unreviewed scratch journal as its source and the vault
	// reported perfectly healthy. The citation looked rigorous and was not
	// (issue #137).
	//
	// A POINTER so absent means "not declared", which resolves to true: every
	// vault predating this field keeps working unchanged, and a type only stops
	// being citable when someone says so. Declared, never inferred — the same
	// rule capability profiles follow.
	Authoritative *bool `yaml:"authoritative,omitempty" json:"authoritative,omitempty"`
}

TypeDef defines a note type in the registry.

func GapTypeDef added in v0.10.13

func GapTypeDef() TypeDef

GapTypeDef is the built-in gap type. No template path: the minimal template is enough, and an existing vault does not have a gap template file on disk. A gap is not citable evidence. Its statuses are open and closed, never draft.

type VaultConfig

type VaultConfig struct {
	Exclude []string `yaml:"exclude"`
}

VaultConfig holds vault scanning settings.

Jump to

Keyboard shortcuts

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