RFD 0003: SemVer, capability model, Sigstore signing¶
Summary¶
Samuel v2 adopts three industry standards as-is for plugin versioning, security, and compatibility:
- SemVer 2.0.0 for every version field (framework, plugin protocol, individual plugins).
- Cargo-style version ranges for dependency specifications (
^1.0.0,~1.2.0,>=1.0.0,<2.0.0). - Sigstore / cosign for plugin signing — signed-by-default for the official registry,
--allow-unsignedfor development.
Plus one Samuel-defined system:
- Capability model — every executable plugin declares the filesystem, exec, and network permissions it needs. Users see and grant capabilities at install time. The runtime enforces them at the sandbox boundary.
This RFD locks the manifest schema (samuel-plugin.toml) and lockfile schema (samuel.lock) that every plugin and project uses. All three plugin tiers from [[0001|RFD 0001]] satisfy this schema.
Problem statement¶
A plugin system needs to answer four interlocking questions:
- How do versions relate? When the framework is v2.3.1 and a plugin declares "I support v2.x", does it work? When a plugin depends on another plugin "^1.0.0", how does the resolver pick a version?
- How does the framework promise backward compatibility? A v2.0 plugin installed today should still work under v2.5 next year. What does that guarantee look like?
- How do users know what a plugin can do? Installing a plugin that reads
/workspaceis fine. Installing one that writes anywhere on disk + calls arbitrary network endpoints is not. The user must see this before agreeing. - How does the user know the plugin is what it claims to be? A typosquatted plugin (
samuel-go-guide-typo) impersonating an official one is a real attack. Signature verification distinguishes legitimate from malicious.
v1 has none of this — skills are static slices in Go code, no signatures, no capabilities, version is whatever the binary embeds. v2 needs all four because the plugin ecosystem invites third-party authors.
Requirements¶
- Plugin manifest is a single TOML file with a small, learnable schema.
- Version comparison and range resolution match industry-standard semantics so plugin authors don't learn yet another scheme.
- Capability grants are explicit at install time, recorded in the lockfile, audited at runtime.
- Plugin signatures are verifiable without external tooling beyond what Samuel ships embedded (sigstore-go).
- The framework's compatibility promise is documented, falsifiable, and enforced in CI.
- All four mechanisms compose: a SemVer-ranged dependency on a signed plugin with declared capabilities can be resolved and verified in one operation.
Constraints¶
- Plugin authors should not have to learn Samuel-specific versioning quirks. SemVer is universal; we don't invent.
- Capability strings need to be machine-readable and human-readable.
- Sigstore keyless signing requires GitHub Actions OIDC tokens — workable for our deployment model.
- The lockfile is committed to git; it must be deterministic, reviewable in PR diffs, and version-pinable.
Background¶
Version-and-compatibility patterns in the ecosystem¶
Cargo (Rust): Cargo.toml declares dependencies with ranges (^1.0, ~1.0, >=1.0); Cargo.lock records exact resolved versions. Used by every Rust project. The semantics are well-documented and well-understood.
npm (JavaScript): package.json + package-lock.json. Similar to Cargo but with quirkier range semantics (^0.x is special-cased, prerelease handling varies). Industry-standard despite the quirks.
pip + uv (Python): PEP 440 version specifiers (>=1.0,<2.0, ~=1.0), pyproject.toml for declaration, uv.lock for the resolved state. Newer, cleaner than older Python tooling.
Go modules: go.mod with minimum-version selection (MVS), no version ranges in the npm/cargo sense. Different philosophy. Doesn't fit our plugin ecosystem because plugins are independent of each other and the framework, not a unified module graph.
We follow Cargo's model because (a) it's mature, (b) Go developers are increasingly familiar with it from cross-ecosystem work, and © the range semantics are unambiguous.
Capability-permission patterns¶
Web browsers: declarative manifests (manifest.json for Chrome extensions) listing permissions (activeTab, storage, <all_urls>). User sees the list at install time, accepts or declines. Permissions enforced at runtime by the browser.
Mobile (iOS/Android): similar — manifests declare permissions, OS enforces. iOS adds run-time prompts for sensitive ones.
Kubernetes RBAC: explicit role-based grants, fine-grained, audit-logged.
WASM Component Model: the upcoming spec defines capability-based imports — a WASM module can only call host functions it imports, and the host decides which imports to satisfy.
Samuel's capability model takes from browsers (declarative manifest, install-time grant, runtime enforcement) and WASM (capability-gated host functions).
Signing patterns¶
npm + Sigstore: as of 2024, npm packages can be signed via Sigstore keyless signing. npm publish --provenance attaches a signed attestation.
Container images + cosign: increasingly standard for OCI registries. cosign sign --keyless uses GitHub Actions OIDC; verifies through Sigstore's transparency log (Rekor).
Linux distros: package signing via long-lived GPG keys. Mature, well-understood, but key management is painful.
Sigstore keyless is the modern standard. No private keys to rotate, no compromise risk from key theft. Identity comes from the workflow that signed (e.g., GitHub Actions OIDC token tied to a specific repo + workflow + ref). Verifiers check the workflow identity matches an expected pattern.
Samuel uses Sigstore keyless for the same reasons npm and cosign adopted it — no key management for plugin authors, strong identity binding to the publish workflow.
v1's gestures toward these requirements¶
- v1 has a
compatibilityfield in SKILL.md frontmatter (per [[../../wiki/entities/skill-md]]), but it's never enforced. - v1's CI doesn't sign release artifacts.
- v1 has no capability concept — every skill is just text the agent reads.
- v1 pins its gstack dependency to a specific SHA ([[../../wiki/entities/component-gstack-gbrain]]) — the right pattern, applied once, ad hoc.
The right pattern existed in flashes. v2 formalizes it.
Options considered¶
Option A: SemVer 2.0 + Cargo ranges + capability manifest + Sigstore keyless (chosen)¶
Adopt three industry standards (SemVer, Cargo ranges, Sigstore) and define one Samuel-specific layer (capability model) on top.
Pros: - Standards inherit ecosystem tooling: Cargo developers immediately understand ^2.0.0. Sigstore tooling has documentation, libraries, transparency logs. - Plugin authors don't learn Samuel-specific versioning. - Capability model maps to mental models from browser extensions and mobile apps. - All four pieces are independently mature; we compose them, not invent them. - Keyless signing eliminates key-management burden for plugin authors.
Cons: - Three standards to know (mitigated: they're already industry-standard). - Capability strings must be carefully designed to avoid being too coarse (exec) or too fine (exec("ls", ["-la", "/workspace"])). - Sigstore keyless ties verification to the Sigstore transparency log being up. Mitigated by sigstore-go's bundled trust root + caching.
Effort: Medium. Manifest parser and capability gate are new code (~2 weeks). SemVer ranges via existing Go library (golang.org/x/mod/semver plus a thin wrapper for cargo-range syntax). Sigstore via sigstore-go.
Option B: Calendar versioning (CalVer) for the framework, SemVer for plugins¶
The framework uses YYYY.MM.PATCH (like Ubuntu, Black, pip). Plugins still use SemVer.
Pros: - Date in the version makes "is this current?" immediate. - Avoids the "do we need a major bump?" debate for the framework — every year ticks the major.
Cons: - Mismatched conventions between framework and plugins is confusing. - Compatibility ranges (framework = "^2026.05") read awkwardly. - Most users won't care about the date as much as the API stability — SemVer telegraphs breaking changes more clearly.
Effort: Same. But the inconsistency cost is real.
Option C: Exact pins only — no version ranges¶
Every dependency pins to an exact version. No ^1.0.0. Just 1.4.2.
Pros: - Maximum reproducibility. - Simpler resolver (no version-range arithmetic).
Cons: - Every minor plugin update requires a coordinated re-pin across every dependent plugin and the registry index. - No room for security patches — a plugin pinned to 1.4.2 doesn't auto-pick up 1.4.3 even though the patch is compatible. - Breaks the standard ecosystem expectation (Cargo, npm, Maven all use ranges).
Effort: Lower. But the ecosystem friction is severe.
Option D: Browser-style runtime permission prompts (not declarative)¶
Plugins request capabilities at runtime — Samuel pauses, asks the user, grants or denies. Like iOS's "Allow this app to access your photos" dialog.
Pros: - Maximum granularity. User sees exactly the moment a capability is needed.
Cons: - Hostile UX for CLI automation. CI scripts can't answer permission prompts. - A misbehaving plugin can ask for capabilities repeatedly until the user clicks "yes." - No audit trail of what was granted (vs lockfile-recorded declarative grants). - Plugin authors can't know in advance what they're allowed to do — leads to defensive over-asking.
Effort: Medium. Worse UX. Rejected.
Option E: No signing required (trust GitHub repo only)¶
Skip Sigstore. A plugin's authenticity is "it's in the official registry and the registry is committed by an authorized account."
Pros: - Less complexity. - No dependency on Sigstore infrastructure.
Cons: - An attacker who compromises the registry repo can publish typosquatted plugins. Or a stolen contributor token publishes a malicious version. - No verification that the binary the user installed matches what the plugin author intended (GHCR can serve a different image than the manifest expected — unlikely but in scope for a serious threat model). - The industry has moved past "git commit signing is enough" — supply chain attacks on package registries are the modern threat (npm IconBurst, PyPI typosquatting, etc.).
Effort: Lower. But security posture is materially weaker.
Option F: One unified version axis — framework, protocol, plugin all share a number¶
Bump the framework version means bumping the protocol version means bumping every plugin's compatibility range.
Pros: - One number to think about.
Cons: - Conflates orthogonal concerns. A UX-only framework patch bumps the protocol unnecessarily. Plugins force-rev for cosmetic reasons. - Loses the ability to ship framework UX changes without plugin churn.
Effort: Lower in mechanics but higher in long-term cost.
Decision¶
Adopt Option A: SemVer 2.0 + Cargo-style ranges + declarative capability manifest + Sigstore keyless signing.
The decision rests on four judgments:
-
Industry standards are free leverage. SemVer, Cargo ranges, and Sigstore have documentation, tooling, mental models, and ecosystem familiarity already built. Samuel inherits all of it by adopting them as-is.
-
Three version axes prevent unnecessary coupling. Framework version, plugin protocol version, and individual plugin versions evolve at different rates. Coupling them (Option F) means UX changes force protocol bumps; protocol changes force plugin republishes. Keeping them separate keeps each release tight.
-
Declarative capabilities scale to automation. CI scripts run
samuel installnon-interactively. Browser-style runtime prompts (Option D) break that. Declarative manifests +--yesflag preserve automation while keeping the audit trail insamuel.lock. -
Sigstore keyless is the security posture v2 deserves. No keys for plugin authors to manage. Strong identity binding (workflow + repo + ref). Aligns with how npm and OCI registries are moving. Opt-out via
--allow-unsignedfor development and unsigned community plugins.
Implementation plan¶
Phase 1 — manifest schema (PRD 0002, week 4)¶
The canonical samuel-plugin.toml:
# Identity
name = "go-guide" # MUST match the plugin's directory name
version = "1.4.2" # SemVer 2.0.0
kind = "skill" # "skill" | "wasm" | "oci"
# Framework / protocol compatibility
[samuel]
framework = "^2.0.0" # Cargo range — framework versions supported
protocol = "^1.0.0" # Cargo range — plugin protocol versions supported
# What this plugin contributes
[provides]
skills = ["go-guide"] # SKILL.md names this plugin ships
commands = [] # CLI subcommands this plugin adds (rare)
methodology = [] # methodology names (rare; e.g., "tdd-strict")
hooks = [] # hook names this plugin attaches to
# Plugin dependencies (resolved via the registry)
[requires]
# other-plugin = "^1.0.0"
# Capabilities the plugin needs (empty for pure skill plugins)
[capabilities.filesystem]
read = ["/workspace"]
write = [] # e.g. ["/workspace/**/AGENTS.md"]
[capabilities]
exec = false # bool — can spawn host subprocesses
[capabilities.network]
outbound = [] # list of allowed hosts; empty = no network
# Tier-specific blocks
# kind = "wasm" only:
[wasm]
module = "plugin.wasm"
exports = ["init", "run", "health"]
runtime = { fuel = 100_000_000, memory_pages = 256 }
# kind = "oci" only:
[oci]
image = "ghcr.io/samuelpkg/samuel-runner-claude:1.0.0"
# digest populated at install time, recorded in samuel.lock
# Optional metadata for discovery and auto-load
[metadata]
description = "Go language guardrails and patterns"
license = "MIT"
homepage = "https://github.com/samuelpkg/samuel-go-guide"
category = "language" # "language" | "framework" | "workflow" | "tool" | "agent"
language = "go" # for auto-load via file extension
extensions = [".go"] # files of these extensions trigger auto-load
auto_load = true
tags = ["go", "golang", "guardrails"]
The schema is small. Plugin authors fill in name, version, kind, framework/protocol ranges, and the capabilities their plugin needs. Everything else is optional.
Phase 2 — SemVer + cargo-range library (PRD 0002, week 4-5)¶
internal/plugin/semver/:
- Wrap
golang.org/x/mod/semverfor parsing and comparison. - Implement Cargo-style range parsing:
^X.Y.Z— compatible-with — allows updates that do not modify the leftmost non-zero element.~X.Y.Z— tilde requirements — allows patch-level changes when minor is specified.>=X,<Y— explicit range.*— any version.X.Y.Z(no operator) — exact, equivalent to=X.Y.Z.- Test against the canonical Cargo test cases.
Phase 3 — capability model + grant flow (PRD 0003, week 1)¶
internal/plugin/capabilities/:
Capability namespace (initial set; designed to grow):
| Capability | Format | Granularity |
|---|---|---|
filesystem.read | list of path globs | per-glob |
filesystem.write | list of path globs | per-glob |
exec | bool | all-or-nothing (rare; flagged in UX as high-risk) |
network.outbound | list of hosts | per-host |
samuel.api | list of API names (e.g., ["plugin.install", "config.read"]) | per-call |
assistant.invoke | list of agent names (e.g., ["claude", "codex"]) | per-agent |
Grant flow at samuel install:
- Parse the plugin manifest.
- If
--yesflag set, grant all capabilities silently and proceed. - Otherwise: render the capability list with risk hints:
- On grant, record in
samuel.lock:
Runtime enforcement:
- WASM plugins: host functions (
samuel.fs.read(path), etc.) check the lockfile's granted capabilities before performing the action. Unauthorized calls return error. - OCI plugins: container started with mounts/network corresponding to granted capabilities. Filesystem mounts are
:roor:rwper the grant. Network namespaces locked down to the allowlist. - Skill plugins: no execution; capabilities are advisory (the user sees what files this plugin's content will be installed to).
Phase 4 — Sigstore signing in release workflow (PRD 0001, week 2)¶
The reusable plugin release workflow ([[0007|RFD 0007]]) signs every release artifact:
# Reusable: github.com/samuelpkg/samuel-plugin-release/.github/workflows/release.yml
- uses: sigstore/cosign-installer@v3
- name: Sign blob (skill / WASM)
if: matrix.kind != 'oci'
run: |
# --new-bundle-format emits the sigstore-go protobuf JSON bundle.
# The legacy --bundle output is silently rejected by the verifier.
cosign sign-blob --yes --new-bundle-format \
--bundle ${{ matrix.artifact }}.bundle ${{ matrix.artifact }}
- name: Sign image (OCI)
if: matrix.kind == 'oci'
run: |
cosign sign ${{ matrix.image }}
Keyless via GitHub Actions OIDC. Identity recorded: workflow + repo + ref. No keys to manage.
Phase 5 — Sigstore verification at install (PRD 0003, week 2-3)¶
internal/plugin/verify/ uses sigstore-go:
- Skill / WASM:
cosign verify-blobequivalent against the.bundlefile alongside the artifact. - OCI:
cosign verifyequivalent against the image digest.
Verification policy in samuel.toml:
[security]
signed_default = true # require signature for registry plugins
allow_unsigned_for = ["local", "dev"] # exceptions
trusted_root = "https://tuf-repo-cdn.sigstore.dev" # Sigstore's published root
identity_pattern = "https://github.com/samuelpkg/samuel-*/.github/workflows/release.yml@refs/heads/main"
The identity_pattern is a regex against the OIDC identity embedded in the signature. v2 ships with the pattern above as default — every plugin published via the reusable workflow signs with this identity. Users with custom registries set their own pattern.
--allow-unsigned CLI flag overrides for individual installs (logged as warning).
Phase 6 — compatibility promises in CI (Milestone 6)¶
The framework's compatibility promise:
- MAJOR (2.x → 3.x): breaking changes allowed. Migration guide required. One full minor cycle of deprecation warnings before the major bump.
- MINOR (2.0 → 2.1): additive only. New CLI flags, new manifest fields (optional), new hooks. Existing plugins keep working.
- PATCH (2.0.0 → 2.0.1): bug fixes only.
The plugin protocol's compatibility promise:
- MAJOR (protocol 1 → 2): rare. Requires plugin re-publish. Framework supports the prior protocol major for one full minor cycle.
- MINOR: additive. Plugins targeting protocol
^1.0keep working against protocol 1.1, 1.2, etc.
CI gates enforce these:
- A change touching
internal/plugin/manifest/types triggers a "minor-bump-or-major-bump?" prompt in the PR description. - The CI's compatibility-check job builds the prior minor's plugin examples against the current framework — must pass.
- Removing a public CLI flag without a deprecation cycle fails the check.
Phase 7 — SBOM and SLSA (Milestone 6, post-v2.0)¶
Goreleaser's built-in SBOM generation produces SPDX-format documents per release. SLSA Level 2 provenance attestation comes from the same release workflow. Both are opt-in at v2.0; document the path forward.
Acceptance criteria¶
-
samuel-plugin.tomlparses with the documented schema; reject malformed manifests with structured error pointing to the relevant docs page. - SemVer parser handles all cargo-style range operators.
- Version resolver produces deterministic results given the same registry state.
- Capability prompt renders with risk hints (filesystem.write, exec, network.outbound flagged).
-
samuel install --yesgrants all capabilities silently. -
samuel.lockrecords granted capabilities per plugin. - WASM plugin without
filesystem.writecapability cannot callsamuel.fs.write(host function returns error). - OCI plugin runs in a container with mounts matching its granted capabilities.
- Sigstore signature verification succeeds for a plugin signed via the reusable workflow.
-
samuel install --allow-unsignedpermits unsigned plugins, logs warning. -
samuel install <plugin>@^1.0.0resolves to the highest compatible tag in the registry. - Framework's CI compatibility-check job blocks PRs that would break MINOR compatibility.
-
samuel doctorreports each installed plugin's signature status (verified, unsigned, identity-mismatch).
Compatibility and migration¶
- v1 has no manifest schema. v2's
samuel-plugin.tomlis new for every plugin. Migration script ([[0007|RFD 0007]]) generates manifests from v1 SKILL.md frontmatter. - v1 has no signing. v2 signs every plugin published through the reusable workflow.
- v1 has no capability model. v2 derives defaults for ported plugins: skill-tier plugins get
filesystem.read:/workspaceonly. - v1 has no lockfile. v2's
samuel.lockis committed by users on firstsamuel install.
For plugin authors: a v2 plugin author template (samuel plugin new, deferred to v2.1+) generates a manifest with sensible defaults — skill plugins start with read-only capability, WASM/OCI plugins prompt for their needs.
Risks¶
| Risk | Likelihood | Mitigation |
|---|---|---|
| Sigstore TUF trust root rotation breaks verification | Low | Use sigstore-go's bundled trust root. Re-fetch on samuel update. Document the manual root-rotation path. |
| Capability strings become unwieldy at scale | Medium | Start with a small list (6 capabilities). Adding a new capability is a framework MINOR bump. |
| Cargo-range semantics surprise plugin authors | Low | Document the syntax with examples. Reference the upstream Cargo docs. |
| Identity-pattern verification rejects legitimate plugins | Medium | Default pattern covers the official registry; users can extend. Provide good error messages on identity mismatch ("the plugin was signed by X, but your config expects Y"). |
| Signed-by-default blocks community plugins that aren't signed | High | --allow-unsigned flag. Registry's upstream = true marker auto-allows. Document for users. |
| Capability prompt UX is annoying for high-trust use cases | Medium | --yes flag. samuel.toml [security] auto_grant_for = ["filesystem.read:/workspace"] for sets always-OK. |
| Plugin protocol version drift between framework and plugins | Medium | CI verifies; samuel install reports "this plugin targets protocol X, framework speaks Y" if incompatible. |
Resolved decisions (2026-05-12)¶
-
Capability path glob syntax:
bmatcuk/doublestarGo library for**recursive globs + standardfilepath.Match-compatible patterns. Matches familiargitignore-style mental model. Examples:/workspace/**/*.md,/workspace/**/AGENTS.md. -
Capability prompt skip for "obviously safe" plugins: skill-tier plugins with only
filesystem.read:/workspaceskip the prompt. Surfaced in--verbosemode. Reduces friction for ~60%+ of installs (skill plugins dominate the registry). -
Identity pattern customization: allow multiple patterns OR'd in
samuel.toml [security] identity_patterns = [...]. Users with custom registries add their own. -
Lockfile churn from capability re-grants: accepted. The lockfile is reviewed at install time; PR diff noise is the cost of the audit trail.
-
Plugin signing during local development:
--allow-unsignedcovers v2.0.samuel plugin verify <plugin-path>ships in v2.1+ alongside the plugin authoring CLI. -
Framework version pinning in
samuel.toml: optional fieldsamuel.framework = "2.0.x"supported. Recommended in CI templates. Pattern matches Cargo'srust-version. -
Verification cache: cache
(content-hash → verified-identity)results at~/.samuel/cache/verify/. Invalidate on samuel binary update. Implementation detail of PRD 0003.
Outcome¶
To be filled in after v2.0 implementation. Expected outcomes:
- Plugin authors describe the manifest schema as "small enough to memorize."
- Capability prompts are reviewed and acted on by users (not blind-yes'd) for high-risk plugins (exec, write).
- Sigstore verification catches at least one attempted impersonation in the first year (the threat model the design assumes).
- Cargo-range semantics introduce no surprises that warrant a v2 patch.
Related artifacts¶
- [[0001|RFD 0001]] — three plugin tiers (the kind field this RFD defines)
- [[0005|RFD 0005]] — Plugin interface (the
Manifest()method returns the schema this RFD specifies) - [[0007|RFD 0007]] — plugin migration (generates manifests for the 78 v1 skills)
- PRD 0002 (Core) — implements manifest parser, SemVer ranges, capability model
- PRD 0003 (Plugin Loader) — implements signature verification, capability gates per tier
- [[../../wiki/concepts/versioning-compatibility]] — wiki concept this RFD ports
- [[../../wiki/entities/config-format]] — TOML format decision
- Sigstore — signing infrastructure
- SemVer 2.0.0
- Cargo version requirements