pluginsource

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package pluginsource persists ORG-PRIVATE plugin bindings: "this team's runs get the plugin living in this git repository".

Why it exists. ADR-079 made an enabled plugin's skills reach a cloud runner pod, but it resolves them from the LAUNCHING instance's iterion home — and a cloud server pod's home is ephemeral too. An operator who installs a private plugin there loses it on the next restart, silently: the mirror is best-effort, so runs simply proceed without the skill and produce a plausible-looking wrong result. There is also no way to scope a plugin to one org: enablement is a global per-instance toggle.

A PluginSource fixes both by moving the AUTHORITY off the pod's filesystem: the durable record (in Mongo, cloud state) says which git repo holds the plugin and which stored secret reads it. The checkout is only ever a re-derivable cache. Team-scoped, so each org brings its own private plugins.

The credential is referenced, never inlined: SecretID points at a GenericSecret (a PAT or deploy key) the fetcher consumes without the value passing through this package.

Index

Constants

View Source
const CollectionName = "plugin_sources"

CollectionName is the Mongo collection backing plugin sources.

This collection is the whole point of the package: it is the DURABLE authority a cloud pod's ephemeral filesystem cannot be. A restarted server re-derives its plugin checkouts from here instead of silently losing them.

View Source
const FetchTimeout = 90 * time.Second

FetchTimeout bounds a single git operation. A plugin fetch sits on the launch path, so a hung remote must fail fast and loudly rather than stall a run submission indefinitely.

Variables

View Source
var (
	ErrNotFound      = errors.New("pluginsource: not found")
	ErrTenantMissing = errors.New("pluginsource: store called without tenant context")
	ErrNameConflict  = errors.New("pluginsource: a source with this name already exists for the team")
)

Functions

func ValidName

func ValidName(name string) error

ValidName mirrors the plugin registry's naming rule so a source cannot persist a name the registry would later reject: lowercase alphanumerics plus '-' and '_', no path separators (the name becomes a directory).

Types

type Fetcher

type Fetcher struct {
	// CacheDir roots the checkouts. Ephemeral by design: it is a cache, never
	// the authority — the durable record is the PluginSource in the store, so
	// a cold pod simply re-derives.
	CacheDir string
	// CredentialFor resolves a source's read credential. It returns the
	// secret VALUE, which the fetcher passes to git without logging it.
	// Nil (or a nil return) means "public repository".
	CredentialFor func(ctx context.Context, s PluginSource) (string, error)
}

Fetcher materialises a PluginSource's repository into a local cache directory and returns the checkout path.

Caching is keyed by (git_url, ref) and, once resolved, the tree is left in place: with a PINNED ref the content is immutable, so the second launch and every one after it cost nothing. That is the whole reason the design prefers pinning over a moving branch — it collapses "resolve at launch" into a no-network operation without introducing a staleness window.

func (*Fetcher) Fetch

func (f *Fetcher) Fetch(ctx context.Context, s PluginSource) (string, error)

Fetch returns a local path containing the source's repository at its ref.

type File

type File struct {
	// Kind is the .claude/ leaf dir: "skills" | "commands" | "agents".
	Kind    string
	Name    string
	Content []byte
}

File is one markdown contribution read out of a git-hosted plugin, in the shape the run-contribution payload expects.

type MemoryStore

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

MemoryStore is an in-process Store for tests and local mode.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

func (*MemoryStore) Create

func (m *MemoryStore) Create(_ context.Context, s PluginSource) error

func (*MemoryStore) Delete

func (m *MemoryStore) Delete(_ context.Context, id string) error

func (*MemoryStore) Get

func (*MemoryStore) ListByTenant

func (m *MemoryStore) ListByTenant(_ context.Context, tenantID string) ([]PluginSource, error)

func (*MemoryStore) ListEnabledByTenant

func (m *MemoryStore) ListEnabledByTenant(ctx context.Context, tenantID string) ([]PluginSource, error)

func (*MemoryStore) Update

func (m *MemoryStore) Update(_ context.Context, s PluginSource) error

type MongoStore

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

MongoStore is the cloud-mode Store.

func NewMongoStore

func NewMongoStore(db *mongo.Database) *MongoStore

func (*MongoStore) Create

func (s *MongoStore) Create(ctx context.Context, ps PluginSource) error

func (*MongoStore) Delete

func (s *MongoStore) Delete(ctx context.Context, id string) error

func (*MongoStore) EnsureSchema

func (s *MongoStore) EnsureSchema(ctx context.Context) error

func (*MongoStore) Get

func (s *MongoStore) Get(ctx context.Context, id string) (PluginSource, error)

func (*MongoStore) ListByTenant

func (s *MongoStore) ListByTenant(ctx context.Context, tenantID string) ([]PluginSource, error)

func (*MongoStore) ListEnabledByTenant

func (s *MongoStore) ListEnabledByTenant(ctx context.Context, tenantID string) ([]PluginSource, error)

func (*MongoStore) Update

func (s *MongoStore) Update(ctx context.Context, ps PluginSource) error

type PluginSource

type PluginSource struct {
	ID       string `bson:"_id" json:"id"`
	TenantID string `bson:"tenant_id" json:"tenant_id"`
	// Name is the plugin name as it will appear in the registry. It must
	// match the plugin.yaml `name` when the repo carries a manifest; for a
	// bare skills/ repo it names the synthesized skills-only plugin.
	Name string `bson:"name" json:"name"`
	// GitURL is the clone URL (https or ssh, per the credential kind).
	GitURL string `bson:"git_url" json:"git_url"`
	// Ref is what to check out. PIN A TAG OR SHA in production: a moving
	// branch turns every launch into a network round-trip AND makes the
	// skill change under the operator's feet, which is the silent-drift
	// failure this whole area exists to prevent. A pinned ref makes the
	// cache immutable and updates an explicit, auditable act.
	Ref string `bson:"ref" json:"ref"`
	// SecretID references the GenericSecret holding the read credential
	// (PAT or deploy key). Empty for a public repository.
	SecretID string `bson:"secret_id,omitempty" json:"secret_id,omitempty"`
	// Enabled gates whether this source contributes to the team's runs.
	Enabled bool `bson:"enabled" json:"enabled"`

	CreatedBy string    `bson:"created_by" json:"created_by"`
	CreatedAt time.Time `bson:"created_at" json:"created_at"`
	UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}

PluginSource binds a team to a plugin hosted in a git repository.

func (*PluginSource) PinnedRef

func (s *PluginSource) PinnedRef() bool

PinnedRef reports whether Ref looks like an immutable pin (a full sha or a tag) rather than a moving branch. Callers use it to warn: a moving ref is allowed but means the plugin can change under a run without any operator action.

func (*PluginSource) Validate

func (s *PluginSource) Validate() error

Validate checks a source before it is persisted. Errors are explicit — a malformed source must fail at write time, not silently contribute nothing at launch time.

type Resolver

type Resolver struct {
	Store   Store
	Fetcher *Fetcher
	// Warnf reports a source that could not be resolved. Optional.
	Warnf func(format string, args ...any)
}

Resolver turns a team's enabled PluginSources into contribution files.

This is what makes an org-private plugin DURABLE and TEAM-SCOPED: the authority is the store record (Mongo), not a pod's filesystem, so a restart re-derives instead of silently losing the plugin — and each team resolves only its own sources.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, tenantID string) ([]File, error)

Resolve returns the contribution files for every enabled source of a tenant.

Failure policy is deliberate and differs from the local plugin registry's "best-effort, skip quietly": a source the operator explicitly bound and enabled, which then fails to fetch, is reported as an ERROR. Silently contributing nothing is precisely the failure mode that lets a deploy run without its platform playbook and still report success.

type Store

type Store interface {
	Create(ctx context.Context, s PluginSource) error
	Get(ctx context.Context, id string) (PluginSource, error)
	Update(ctx context.Context, s PluginSource) error
	Delete(ctx context.Context, id string) error
	// ListEnabledByTenant returns the sources a launch must resolve.
	ListEnabledByTenant(ctx context.Context, tenantID string) ([]PluginSource, error)
	ListByTenant(ctx context.Context, tenantID string) ([]PluginSource, error)
}

Store persists plugin sources. Mongo-backed in cloud, memory in tests.

Jump to

Keyboard shortcuts

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