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
- Variables
- func Marshal(m *Manifest) ([]byte, error)
- func NormalizeEmail(email string) string
- func Resolve(m *Manifest, actor mgmt.Actor) *lockfile.LockFile
- func Save(vaultRoot string, m *Manifest) error
- func Write(m *Manifest, path string) error
- type Asset
- type Bot
- type Dependency
- type Manifest
- func (m *Manifest) DeleteBot(name string) error
- func (m *Manifest) DeleteTeam(name string) error
- func (m *Manifest) FindAsset(name string) *Asset
- func (m *Manifest) FindBot(name string) (*Bot, error)
- func (m *Manifest) FindTeam(name string) (*Team, error)
- func (m *Manifest) RemoveAsset(name, version string) int
- func (m *Manifest) TeamsForMember(email string) []*Team
- func (m *Manifest) UpsertAsset(a Asset) *Asset
- func (m *Manifest) UpsertBot(b Bot) (*Bot, error)
- func (m *Manifest) UpsertTeam(t Team) (*Team, error)
- type Scope
- type ScopeKind
- type SourceGit
- type SourceHTTP
- type SourcePath
- type Team
Constants ¶
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.
const FileName = "sx.toml"
FileName is the path, relative to the vault root, where the manifest lives.
Variables ¶
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 ¶
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 ¶
NormalizeEmail lowercases and trims an email for comparison and storage.
func Resolve ¶
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.
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.
type Dependency ¶
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 ¶
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 ¶
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 (*Manifest) DeleteBot ¶ added in v0.16.2
DeleteBot removes the bot by name, returning ErrBotNotFound if missing.
func (*Manifest) DeleteTeam ¶
DeleteTeam removes the team by name, returning ErrTeamNotFound if missing.
func (*Manifest) FindBot ¶ added in v0.16.2
FindBot returns the bot with the given name, or ErrBotNotFound.
func (*Manifest) RemoveAsset ¶
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 ¶
TeamsForMember returns all teams containing the given email as a member.
func (*Manifest) UpsertAsset ¶
UpsertAsset replaces an asset by (name, version) or appends if missing. Returns the pointer into the manifest's slice.
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.
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).