manifest

package
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: May 25, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package manifest defines sx.toml, the source-of-truth file stored at the vault root. It holds the set of assets managed by the vault, their install scopes (org/repo/path/team/user), and the teams (with members, admins, and repositories) those scopes reference.

This package is format-only: parse, marshal, read, write, plus small helper methods for idempotent mutation. I/O locking, git commit/push, and identity resolution are the caller's responsibility.

Index

Constants

View Source
const CurrentSchemaVersion = 1

CurrentSchemaVersion is the schema version written by this build. A future build that bumps the version will migrate v1 files forward on first write; this build rejects files whose version exceeds the one it understands.

View Source
const FileName = "sx.toml"

FileName is the path, relative to the vault root, where the manifest lives.

Variables

View Source
var (
	// ErrUnsupportedSchema is returned when the on-disk file has a schema
	// version this build does not know how to read.
	ErrUnsupportedSchema = errors.New("unsupported manifest schema version")

	// ErrInvalidScopeKind is returned for scope rows with an unrecognized
	// kind field.
	ErrInvalidScopeKind = errors.New("invalid scope kind")

	// ErrEmptyTeamName is returned by team mutators when the normalized team
	// name is blank.
	ErrEmptyTeamName = errors.New("team name cannot be empty")

	// ErrTeamNotFound is returned when a team lookup fails.
	ErrTeamNotFound = errors.New("team not found")

	// ErrTeamExists is returned when CreateTeam finds the team already
	// exists.
	ErrTeamExists = errors.New("team already exists")

	// ErrEmptyBotName is returned by bot mutators when the normalized bot
	// name is blank.
	ErrEmptyBotName = errors.New("bot name cannot be empty")

	// ErrBotNotFound is returned when a bot lookup fails.
	ErrBotNotFound = errors.New("bot not found")

	// ErrBotExists is returned when CreateBot finds the bot already exists.
	ErrBotExists = errors.New("bot already exists")
)

Errors returned by the parser and mutators.

Functions

func Marshal

func Marshal(m *Manifest) ([]byte, error)

Marshal encodes the manifest to TOML bytes with the current schema version written at the top. Fields are normalized (trimmed, deduped, sorted) before encoding so the output is deterministic.

func NormalizeEmail

func NormalizeEmail(email string) string

NormalizeEmail lowercases and trims an email for comparison and storage.

func Resolve

func Resolve(m *Manifest, actor mgmt.Actor) *lockfile.LockFile

Resolve produces a lockfile.LockFile from the manifest, flattening team/user/bot scopes into repo scopes based on the caller's identity. The resulting lock file is the per-user, machine-generated artifact written to the caller's local lockfile cache; the manifest in the vault remains the source of truth.

Resolution rules per scope on each asset (human caller):

  • kind=org → asset is global (no scope restrictions in the lock).
  • kind=repo → one lockfile.Scope with no paths.
  • kind=path → one lockfile.Scope with paths.
  • kind=user, user matches actor.Email → asset is global (mirrors the server-side behavior: a user-specific install makes the asset available anywhere that user works).
  • kind=user, user does not match → scope is silently dropped (belongs to another caller).
  • kind=team, actor is a member → one lockfile.Scope per repository owned by the team (empty paths, i.e. full-repo scope).
  • kind=team, actor is not a member → scope is silently dropped.
  • kind=bot → scope is silently dropped (the human caller is not the named bot).

Bot caller (actor.IsBot()) overrides:

  • kind=org → asset is global.
  • kind=bot, bot matches actor.Bot → asset is global.
  • kind=team → if the bot is on the team (Manifest.Bots row), every repository the team owns becomes a lockfile.Scope. Mirrors skills.new's bot-team membership rule.
  • kind=user → silently dropped (bots are not human users).
  • kind=repo / kind=path → unchanged (bots inherit raw repo scopes same as humans).
  • kind=bot, bot does not match → silently dropped.

Note: we deliberately include kind=org installs in the bot's resolved list. This diverges from the (now-being-fixed) skills.new behavior that excluded org-wide installs from a bot's view. See docs/bots.md.

After accumulating scopes for an asset: if any row produced a global verdict, the asset's Scopes is nil. Otherwise repo-wide and path-restricted entries are deduped per normalized repo URL — a repo-wide entry wins over path-restricted entries for the same repo.

func Save

func Save(vaultRoot string, m *Manifest) error

Save writes the manifest to vaultRoot/sx.toml atomically. Creates the vault root directory if needed.

func Write

func Write(m *Manifest, path string) error

Write writes the manifest to the given absolute path atomically.

Types

type Asset

type Asset struct {
	Name         string       `toml:"name"`
	Version      string       `toml:"version"`
	Type         asset.Type   `toml:"type"`
	Clients      []string     `toml:"clients,omitempty"`
	Dependencies []Dependency `toml:"dependencies,omitempty"`

	SourceHTTP *SourceHTTP `toml:"source-http,omitempty"`
	SourcePath *SourcePath `toml:"source-path,omitempty"`
	SourceGit  *SourceGit  `toml:"source-git,omitempty"`

	// Scopes enumerates every install target for this asset. An empty
	// slice means org-wide / global — the asset is available to every
	// caller regardless of identity or repo. See ScopeKind for the set of
	// permitted kinds.
	Scopes []Scope `toml:"scopes,omitempty"`
}

Asset is one managed asset: its identity, source, and install scopes.

type Bot added in v0.16.2

type Bot struct {
	Name        string   `toml:"name"`
	Description string   `toml:"description,omitempty"`
	Teams       []string `toml:"teams,omitempty"`
}

Bot is a non-human service identity. Bots gain repository context by being members of one or more teams; assets can also be installed directly to a bot via ScopeKindBot. File-based vaults treat bots as identity-only — the trust boundary is "vault read access ⇒ asset access", so anyone with vault access can claim any bot identity.

func (*Bot) IsOnTeam added in v0.16.2

func (b *Bot) IsOnTeam(name string) bool

IsOnTeam returns true if the bot lists the given team in its Teams slice.

type Dependency

type Dependency struct {
	Name    string `toml:"name"`
	Version string `toml:"version,omitempty"`
}

Dependency is a reference to another asset.

type Manifest

type Manifest struct {
	// SchemaVersion gates on-disk compatibility. Bumped for breaking
	// schema changes so newer builds can recognize and migrate older
	// files.
	SchemaVersion int `toml:"schema_version"`

	// CreatedBy records the sx build that last wrote the file. Purely
	// informational — used for diagnostics, not gating.
	CreatedBy string `toml:"created_by,omitempty"`

	// Assets are the assets managed by this vault, including their
	// install scopes.
	Assets []Asset `toml:"assets,omitempty"`

	// Teams are group definitions (members, admins, repositories)
	// referenced by team-scoped installs.
	Teams []Team `toml:"teams,omitempty"`

	// Bots are non-human service identities. A bot can be a member of one
	// or more teams (gaining repo context the same way a human team
	// member does) and can also be a direct install target via
	// ScopeKindBot.
	Bots []Bot `toml:"bots,omitempty"`
}

Manifest is the on-disk structure of sx.toml.

func Load

func Load(vaultRoot string) (*Manifest, bool, error)

Load reads the manifest at vaultRoot/sx.toml. Returns (nil, false, nil) when the file does not exist; callers can use the bool to distinguish "never initialized" from "parse error".

func LoadOrMigrate

func LoadOrMigrate(vaultRoot string) (*Manifest, bool, error)

LoadOrMigrate returns the manifest at vaultRoot/sx.toml, creating it from a legacy sx.lock if sx.toml does not yet exist. The returned bool is true when a migration ran on this call.

After a successful migration, the legacy sx.lock is renamed to sx.lock.migrated so it's preserved on disk but no longer read. If no legacy file exists either, an empty manifest is written and returned.

func Parse

func Parse(data []byte) (*Manifest, error)

Parse parses a manifest from TOML bytes.

func ReadFile

func ReadFile(path string) (*Manifest, error)

ReadFile reads and parses the manifest file at the given absolute path.

func (*Manifest) DeleteBot added in v0.16.2

func (m *Manifest) DeleteBot(name string) error

DeleteBot removes the bot by name, returning ErrBotNotFound if missing.

func (*Manifest) DeleteTeam

func (m *Manifest) DeleteTeam(name string) error

DeleteTeam removes the team by name, returning ErrTeamNotFound if missing.

func (*Manifest) FindAsset

func (m *Manifest) FindAsset(name string) *Asset

FindAsset returns the first asset with the given name, or nil.

func (*Manifest) FindBot added in v0.16.2

func (m *Manifest) FindBot(name string) (*Bot, error)

FindBot returns the bot with the given name, or ErrBotNotFound.

func (*Manifest) FindTeam

func (m *Manifest) FindTeam(name string) (*Team, error)

FindTeam returns the team with the given name, or ErrTeamNotFound.

func (*Manifest) RemoveAsset

func (m *Manifest) RemoveAsset(name, version string) int

RemoveAsset removes every entry matching name. If version is non-empty, only matching versions are removed. Returns the number of rows removed.

func (*Manifest) TeamsForMember

func (m *Manifest) TeamsForMember(email string) []*Team

TeamsForMember returns all teams containing the given email as a member.

func (*Manifest) UpsertAsset

func (m *Manifest) UpsertAsset(a Asset) *Asset

UpsertAsset replaces an asset by (name, version) or appends if missing. Returns the pointer into the manifest's slice.

func (*Manifest) UpsertBot added in v0.16.2

func (m *Manifest) UpsertBot(b Bot) (*Bot, error)

UpsertBot inserts or replaces a bot keyed by name. Returns the pointer into the manifest's own slice, or ErrEmptyBotName if the normalized name is blank.

func (*Manifest) UpsertTeam

func (m *Manifest) UpsertTeam(t Team) (*Team, error)

UpsertTeam inserts or replaces a team keyed by name. Returns the pointer into the manifest's own slice, or ErrEmptyTeamName if the normalized name is blank.

type Scope

type Scope struct {
	Kind  ScopeKind `toml:"kind"`
	Repo  string    `toml:"repo,omitempty"`
	Paths []string  `toml:"paths,omitempty"`
	Team  string    `toml:"team,omitempty"`
	User  string    `toml:"user,omitempty"`
	Bot   string    `toml:"bot,omitempty"`
}

Scope is one install target. Which fields are significant depends on Kind; unused fields are omitted from the TOML output.

func (*Scope) Validate

func (s *Scope) Validate() error

Validate returns nil if this scope row has the fields required by its Kind.

type ScopeKind

type ScopeKind string

ScopeKind identifies the type of an install scope. The manifest represents all five kinds uniformly.

const (
	// ScopeKindOrg means the asset is available org-wide. Equivalent to
	// an asset with no scopes at all; writing it explicitly produces a
	// row in the file instead of an empty slice.
	ScopeKindOrg ScopeKind = "org"

	// ScopeKindRepo means the asset is available for a single repository.
	// The Repo field must be set.
	ScopeKindRepo ScopeKind = "repo"

	// ScopeKindPath means the asset is available for specific paths within
	// a repository. Both Repo and Paths must be set.
	ScopeKindPath ScopeKind = "path"

	// ScopeKindTeam means the asset is available to every member of the
	// named team. The team is defined in Manifest.Teams; the vault layer
	// resolves it against the caller's identity when producing a lock
	// file.
	ScopeKindTeam ScopeKind = "team"

	// ScopeKindUser means the asset is available to a single user,
	// identified by email.
	ScopeKindUser ScopeKind = "user"

	// ScopeKindBot means the asset is available to a single bot identity,
	// identified by name. The bot is defined in Manifest.Bots; the vault
	// layer resolves it against the caller's identity (typically SX_BOT)
	// when producing a lock file.
	ScopeKindBot ScopeKind = "bot"
)

type SourceGit

type SourceGit struct {
	URL          string `toml:"url"`
	Ref          string `toml:"ref"`
	Subdirectory string `toml:"subdirectory,omitempty"`
}

SourceGit describes a git repository source.

type SourceHTTP

type SourceHTTP struct {
	URL    string            `toml:"url"`
	Hashes map[string]string `toml:"hashes"`
	Size   int64             `toml:"size,omitempty"`
}

SourceHTTP describes an HTTP-hosted asset archive.

type SourcePath

type SourcePath struct {
	Path string `toml:"path"`
}

SourcePath describes a local path source.

type Team

type Team struct {
	Name         string   `toml:"name"`
	Description  string   `toml:"description,omitempty"`
	Members      []string `toml:"members,omitempty"`
	Admins       []string `toml:"admins,omitempty"`
	Repositories []string `toml:"repositories,omitempty"`
}

Team is a named group with a member list, admin list, and repositories. Description is optional. Members and Admins are email lists; Admins is expected to be a subset of Members (enforced by callers, not the parser).

func (*Team) IsAdmin

func (t *Team) IsAdmin(email string) bool

IsAdmin returns true if the given email is in the team's admin list. Comparison is case-insensitive.

func (*Team) IsMember

func (t *Team) IsMember(email string) bool

IsMember returns true if the given email is in the team's member list. Comparison is case-insensitive.

Jump to

Keyboard shortcuts

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