Documentation
¶
Overview ¶
Package pluginkit provides the core plugin kit functionality for building Privateer plugins.
Package pluginkit provides the core plugin kit functionality for building Privateer plugins.
Each error factory wraps one of the two category sentinels below so ExitCodeFor can classify it into the right exit code via errors.Is.
Index ¶
- Constants
- Variables
- func AddEvaluationSuiteTyped[S ~func(T) (gemara.Result, string, gemara.ConfidenceLevel), T any](v *EvaluationOrchestrator, catalogId string, loader DataLoader, ...) error
- func AddEvaluationSuiteTypedForAllCatalogs[S ~func(T) (gemara.Result, string, gemara.ConfidenceLevel), T any](v *EvaluationOrchestrator, loader DataLoader, steps map[string][]S) error
- func ExitCodeFor(orch *EvaluationOrchestrator, mobilizeErr error) int
- func FuncName(fn any) string
- type APICallCounter
- type APICallReporter
- type ApplyFunc
- type BenchmarkReport
- type Change
- type ChangeManager
- func (cm *ChangeManager) AddChange(changeName string, change Change)
- func (cm *ChangeManager) Allow()
- func (cm *ChangeManager) Apply(changeName string, targetName string, changeInput any) (success bool, target any)
- func (cm *ChangeManager) Revert(changeName string)
- func (cm *ChangeManager) RevertAll()
- type DataLoader
- type EvaluatesDeclaration
- type EvaluationOrchestrator
- func (v *EvaluationOrchestrator) AddEvaluationSuite(catalogId string, loader DataLoader, steps map[string][]gemara.AssessmentStep) error
- func (v *EvaluationOrchestrator) AddEvaluationSuiteForAllCatalogs(loader DataLoader, steps map[string][]gemara.AssessmentStep) error
- func (v *EvaluationOrchestrator) AddLoader(loader DataLoader)
- func (v *EvaluationOrchestrator) AddReferenceCatalogs(dataDir string, files embed.FS) error
- func (v *EvaluationOrchestrator) AddRequiredVars(vars []string)
- func (v *EvaluationOrchestrator) AddTargetBuilder(builder TargetBuilder)
- func (v *EvaluationOrchestrator) Mobilize() error
- func (v *EvaluationOrchestrator) PublishManifest() (PublishManifest, error)
- func (v *EvaluationOrchestrator) WriteResults() error
- type EvaluationSuite
- type LoaderTiming
- type PublishManifest
- type RevertFunc
- type StepTiming
- type SuiteTiming
- type TargetBuilder
- type TestSet
- type TypedAssessmentStep
Constants ¶
const BenchmarkFileName = "benchmark.json"
BenchmarkFileName is the report's fixed name, written into <write-directory>/<service>/ and located by the harness after a run.
const BenchmarkSchema = "privateer-benchmark/v1"
BenchmarkSchema identifies the benchmark report format for machine consumers.
const PublishManifestCommand = "publish-manifest"
PublishManifestCommand is the plugin subcommand that emits the grc.store publish manifest as JSON. command.NewPluginCommands wires it onto every plugin, and `pvtr publish` execs it on the built binary to read the plugin's coordinate and evaluated catalogs — rather than taking them as flags a non-owner could forge. Shared here so the producer (publish) and the plugin command that emits it can't drift on the name.
Variables ¶
var ( CORRUPTION_FOUND = func(mod string) error { return wrap(ErrRuntime, "target state may be corrupted! Halting to prevent futher damage. See logs for more information", mod) } NO_EVALUATION_SUITES = func(mod string) error { return wrap(ErrDevBug, "no control evaluations provided by the plugin", mod) } EVAL_NAME_MISSING = func(mod string) error { return wrap(ErrDevBug, "evaluationSuite name must not be empty", mod) } CONFIG_NOT_INITIALIZED = func(mod string) error { return wrap(ErrDevBug, "configuration not initialized", mod) } NO_ASSESSMENT_STEPS_PROVIDED = func(mod string) error { return wrap(ErrDevBug, "assessment steps not provided", mod) } NO_ASSESSMENT_REQS_PROVIDED = func(mod string) error { return wrap(ErrDevBug, "assessment requirements not provided", mod) } EVAL_SUITE_CRASHED = func(mod string) error { return wrap(ErrDevBug, "evaluation suite crashed", mod) } )
Error functions that require no parameters.
var ( EVALUATION_ORCHESTRATOR_NAMES_NOT_SET = func(serviceName, pluginName string, mod string) error { return wrap(ErrDevBug, fmt.Sprintf("expected service and plugin names to be set. ServiceName='%s' PluginName='%s'", serviceName, pluginName), mod) } WRITE_FAILED = func(name, err string, mod string) error { return wrap(ErrRuntime, fmt.Sprintf("failed to write results for evaluation suite. name: %s, error: %s", name, err), mod) } BAD_LOADER = func(pluginName string, err error, mod string) error { return wrap(ErrRuntime, fmt.Sprintf("failed to load payload for %s: %s", pluginName, err), mod) } BAD_CATALOG = func(pluginName string, errMsg string, mod string) error { return wrap(ErrDevBug, fmt.Sprintf("malformed data in catalog for %s: %s", pluginName, errMsg), mod) } BAD_EVAL_LOG = func(err error, mod string) error { return wrap(ErrDevBug, fmt.Sprintf("failed to setup evaluation log: %s", err), mod) } BAD_ASSESSMENT_REQS = func(err error, mod string) error { return wrap(ErrDevBug, fmt.Sprintf("failed to load assessment requirements from catalog: %s", err), mod) } BAD_CONFIG = func(err error, mod string) error { return wrap(ErrRuntime, fmt.Sprintf("failed to setup config: %s", err), mod) } NO_MATCHING_CATALOGS = func(requested []string, available []string, mod string) error { return wrap(ErrDevBug, fmt.Sprintf("no requested catalogs matched available suites. requested=%v available=%v", requested, available), mod) } BENCHMARK_WRITE_FAILED = func(err error, mod string) error { return wrap(ErrRuntime, fmt.Sprintf("failed to write benchmark report: %s", err), mod) } )
Error functions that require parameters.
var ErrDevBug = errors.New("privateer plugin development error")
ErrDevBug classifies SDK misuse or malformed plugin data (missing catalogs, unset names, missing assessment steps). Maps to BadUsage.
var ErrRuntime = errors.New("privateer plugin runtime error")
ErrRuntime classifies failures during plugin execution that aren't the plugin author's fault (loader, write, RPC, corruption). Maps to InternalError.
Functions ¶
func AddEvaluationSuiteTyped ¶ added in v1.32.0
func AddEvaluationSuiteTyped[S ~func(T) (gemara.Result, string, gemara.ConfidenceLevel), T any]( v *EvaluationOrchestrator, catalogId string, loader DataLoader, steps map[string][]S, ) error
AddEvaluationSuiteTyped registers an evaluation suite whose steps take a concrete payload type T rather than an untyped any. The SDK performs the payload type assertion once, so plugins need neither a per-step payload guard nor their own adapter — and because the adaptation happens here, each step's real function name survives into the benchmark report.
It is a package-level function rather than a method because Go does not allow type parameters on methods.
All steps in one call share the payload type T; register a second suite for a step family that consumes a different payload.
func AddEvaluationSuiteTypedForAllCatalogs ¶ added in v1.32.0
func AddEvaluationSuiteTypedForAllCatalogs[S ~func(T) (gemara.Result, string, gemara.ConfidenceLevel), T any]( v *EvaluationOrchestrator, loader DataLoader, steps map[string][]S, ) error
AddEvaluationSuiteTypedForAllCatalogs is AddEvaluationSuiteTyped applied to every reference catalog loaded via AddReferenceCatalogs, mirroring AddEvaluationSuiteForAllCatalogs.
func ExitCodeFor ¶ added in v1.24.0
func ExitCodeFor(orch *EvaluationOrchestrator, mobilizeErr error) int
ExitCodeFor maps the outcome of EvaluationOrchestrator.Mobilize into a canonical privateer exit code. Plugin authors call this from Start:
func (p *Plugin) Start() (int, error) {
err := p.orchestrator.Mobilize()
return pluginkit.ExitCodeFor(p.orchestrator, err), err
}
A non-nil error wrapping ErrRuntime → InternalError; ErrDevBug → BadUsage; any other non-nil error → InternalError. With nil error, suite results containing Failed/NeedsReview/Unknown → TestFail; otherwise TestPass.
func FuncName ¶ added in v1.32.0
FuncName resolves a function value's symbol name, as gemara names steps. Exported so plugins can record step identity themselves; the typed registration helpers below already do this on the caller's behalf.
The name can only be resolved while the function is still a symbol. Once a value has been captured into a closure — as happens when a plugin adapts its own steps to gemara.AssessmentStep — every closure produced by that literal shares one code pointer, and the original name is unrecoverable. That is why the adaptation below lives in the SDK: it is the last point at which the plugin's function is still nameable.
Types ¶
type APICallCounter ¶ added in v1.32.0
type APICallCounter struct {
// contains filtered or unexported fields
}
APICallCounter tallies outbound HTTP requests, letting a plugin's payload satisfy APICallReporter without hand-rolling a RoundTripper. Embed a *APICallCounter in the payload and APICallCount is promoted onto it:
type Payload struct {
// ...
*pluginkit.APICallCounter
}
counter := &pluginkit.APICallCounter{}
httpClient := counter.WrapClient(oauth2.NewClient(ctx, src))
return Payload{APICallCounter: counter /* ... */}
Embed the pointer, not the value: payloads are commonly used by value, and a value-embedded counter would be copied so the tallies diverge. Copying is a `go vet` copylocks error rather than a silent miscount, because the tally is an atomic.Int64.
The tally counts HTTP round trips, which is a close proxy for API calls but not identical: retries and redirects each increment it, while one request batching several logical queries (a GraphQL document, say) counts once. It is a rate-limit budget, not an exact call ledger. All hosts share one tally; a plugin calling two rate-limited services sees their sum.
The zero value is ready to use, and a nil *APICallCounter reports zero.
func (*APICallCounter) APICallCount ¶ added in v1.32.0
func (c *APICallCounter) APICallCount() int
APICallCount implements APICallReporter. It is safe on a nil receiver so a payload built without a counter reports zero rather than panicking.
func (*APICallCounter) Wrap ¶ added in v1.32.0
func (c *APICallCounter) Wrap(base http.RoundTripper) http.RoundTripper
Wrap returns base decorated to count every round trip through it. A nil base means http.DefaultTransport, matching http.Client's own behavior. A nil counter returns base undecorated.
func (*APICallCounter) WrapClient ¶ added in v1.32.0
func (c *APICallCounter) WrapClient(client *http.Client) *http.Client
WrapClient decorates client's transport in place and returns client, so it composes with a client an auth library already built. A nil client yields a new one.
type APICallReporter ¶ added in v1.32.0
type APICallReporter interface {
APICallCount() int
}
APICallReporter is an optional interface a plugin may implement to report its cumulative external API-call count.
type ApplyFunc ¶ added in v1.7.0
type ApplyFunc func(interface{}) (interface{}, error)
ApplyFunc is a prepared function to apply a change.
type BenchmarkReport ¶ added in v1.32.0
type BenchmarkReport struct {
Schema string `json:"schema" yaml:"schema"`
PluginName string `json:"plugin-name" yaml:"plugin-name"`
PluginVersion string `json:"plugin-version" yaml:"plugin-version"`
ServiceName string `json:"service-name" yaml:"service-name"`
Loaders []LoaderTiming `json:"loaders" yaml:"loaders"`
Suites []SuiteTiming `json:"suites" yaml:"suites"`
TotalDurationNs int64 `json:"total-duration-ns" yaml:"total-duration-ns"`
APICalls *int `json:"api-calls,omitempty" yaml:"api-calls,omitempty"`
WallClockNs int64 `json:"wall-clock-ns,omitempty" yaml:"wall-clock-ns,omitempty"`
}
type Change ¶
type Change struct {
// TargetName is the name or ID of the resource or configuration that is to be changed
TargetName string `yaml:"target-name"`
// Description is a human-readable description of the change
Description string `yaml:"description"`
// TargetObject is an optional representation of the object that is being changed
TargetObject interface{} `yaml:"target-object,omitempty"`
// Applied is true if the change was successfully applied at least once
Applied bool `yaml:"applied,omitempty"`
// Reverted is true if the change was successfully reverted and not applied again
Reverted bool `yaml:"reverted,omitempty"`
// Error is used if any error occurred during the change
Error error `yaml:"error,omitempty"`
// CorruptedState is true if something went wrong during apply or revert, indicating that the system may be in a bad state
CorruptedState bool `yaml:"bad-state,omitempty"`
// contains filtered or unexported fields
}
Change is a struct that contains the data and functions associated with a single change to a target resource.
func (*Change) AddFunctions ¶ added in v1.7.0
func (c *Change) AddFunctions(applyFunc ApplyFunc, revertFunc RevertFunc)
AddFunctions sets the apply and revert functions for a change.
type ChangeManager ¶ added in v1.7.0
type ChangeManager struct {
// Changes is a map of change names to Change objects, so that multiple changes can be tracked and reused.
Changes map[string]*Change `yaml:"changes"`
// Allowed must be set to true before any change can be applied.
Allowed bool `yaml:"allowed,omitempty"`
// CorruptedState is true if any change has failed to apply or revert, indicating that the system may be in a bad state.
CorruptedState bool `yaml:"bad-state,omitempty"`
}
ChangeManager manages changes that can be applied and reverted during evaluation.
func (*ChangeManager) AddChange ¶ added in v1.7.0
func (cm *ChangeManager) AddChange(changeName string, change Change)
AddChange adds a change to the change manager.
func (*ChangeManager) Allow ¶ added in v1.7.0
func (cm *ChangeManager) Allow()
Allow marks changes as allowed to be applied.
func (*ChangeManager) Apply ¶ added in v1.7.0
func (cm *ChangeManager) Apply(changeName string, targetName string, changeInput any) (success bool, target any)
Apply executes the prepared function for the change. It will not apply the change if it is not allowed, or if it has already been applied and not reverted.
func (*ChangeManager) Revert ¶ added in v1.7.0
func (cm *ChangeManager) Revert(changeName string)
Revert reverts a specific change by name.
func (*ChangeManager) RevertAll ¶ added in v1.7.0
func (cm *ChangeManager) RevertAll()
RevertAll reverts all changes managed by the change manager.
type DataLoader ¶ added in v1.0.1
DataLoader is a function type for loading plugin data from configuration.
type EvaluatesDeclaration ¶ added in v1.31.0
type EvaluatesDeclaration struct {
Catalog string `json:"catalog"` // "<namespace>/<catalog_id>"
CatalogVersion string `json:"catalog_version"` //nolint:tagliatelle // wire contract
RequirementIDs []string `json:"requirement_ids"` //nolint:tagliatelle // wire contract
}
EvaluatesDeclaration is one control-catalog linkage in the publish manifest. The JSON shape matches the OCI config blob's evaluates entry so the publish command can map it across without translation noise.
type EvaluationOrchestrator ¶ added in v1.3.0
type EvaluationOrchestrator struct {
ServiceName string `json:"service-name" yaml:"service-name"`
PluginName string `json:"plugin-name" yaml:"plugin-name"`
PluginUri string `json:"plugin-uri" yaml:"plugin-uri"`
PluginVersion string `json:"plugin-version" yaml:"plugin-version"`
// Publisher is the grc.store author/org id that owns this plugin (and, by
// convention, the catalogs it evaluates). With PluginName it forms the publish
// coordinate <Publisher>/<PluginName>, and it is the namespace of every
// evaluated catalog in the publish manifest. Inert at run time; required only
// to publish (a plugin runs without it, but cannot be published without it).
Publisher string `json:"publisher,omitempty" yaml:"publisher,omitempty"`
// License is the plugin's publication license as an SPDX expression
// (e.g. "Apache-2.0", or "Apache-2.0 OR MIT"; use a LicenseRef-… token for a
// custom license). grc.store requires it on every publication and it is
// recorded in the signed plugin config. Like Publisher it is inert at run time
// and required only to publish; pvtr validates and canonicalizes it (via
// grc-store-protocol/spdx) at publish time, so the plugin only declares the
// raw string here.
License string `json:"license,omitempty" yaml:"license,omitempty"`
// CatalogNamespaces optionally maps a reference-catalog id to the grc.store
// namespace that owns it, for plugins that evaluate catalogs published by
// someone else (e.g. a community plugin evaluating ossf/osps-baseline).
// Catalogs not listed here are assumed to live under Publisher.
CatalogNamespaces map[string]string `json:"catalog-namespaces,omitempty" yaml:"catalog-namespaces,omitempty"`
Payload any `json:"payload,omitempty" yaml:"payload,omitempty"`
Evaluation_Suites []*EvaluationSuite `json:"evaluation-suites" yaml:"evaluation-suites"` // EvaluationSuite is a map of evaluations to their catalog names
// contains filtered or unexported fields
}
EvaluationOrchestrator gets the plugin in position to execute the specified evaluation suites.
func (*EvaluationOrchestrator) AddEvaluationSuite ¶ added in v1.3.0
func (v *EvaluationOrchestrator) AddEvaluationSuite(catalogId string, loader DataLoader, steps map[string][]gemara.AssessmentStep) error
AddEvaluationSuite adds an evaluation suite for the given catalog ID.
Steps registered this way are named by symbol lookup at report time, which only resolves while the step is a plain function value. Plugins that adapt their steps through a closure should register via AddEvaluationSuiteTyped so the SDK captures each step's name before it is captured.
func (*EvaluationOrchestrator) AddEvaluationSuiteForAllCatalogs ¶ added in v1.22.0
func (v *EvaluationOrchestrator) AddEvaluationSuiteForAllCatalogs(loader DataLoader, steps map[string][]gemara.AssessmentStep) error
AddEvaluationSuiteForAllCatalogs registers the provided steps for every reference catalog that has been loaded via AddReferenceCatalogs. This allows plugin developers to define their step implementations once and have them automatically applied to all catalog versions.
func (*EvaluationOrchestrator) AddLoader ¶ added in v1.10.0
func (v *EvaluationOrchestrator) AddLoader(loader DataLoader)
AddLoader sets the data loader function for the orchestrator.
func (*EvaluationOrchestrator) AddReferenceCatalogs ¶ added in v1.10.0
func (v *EvaluationOrchestrator) AddReferenceCatalogs(dataDir string, files embed.FS) error
AddReferenceCatalogs loads reference catalogs from the embedded file system.
func (*EvaluationOrchestrator) AddRequiredVars ¶ added in v1.7.0
func (v *EvaluationOrchestrator) AddRequiredVars(vars []string)
AddRequiredVars sets the required configuration variables for the orchestrator.
func (*EvaluationOrchestrator) AddTargetBuilder ¶ added in v1.31.3
func (v *EvaluationOrchestrator) AddTargetBuilder(builder TargetBuilder)
AddTargetBuilder sets the function that identifies the evaluated resource in each emitted EvaluationLog. Fields left empty by the builder are backfilled from the service name so the log always identifies its target.
func (*EvaluationOrchestrator) Mobilize ¶ added in v1.3.0
func (v *EvaluationOrchestrator) Mobilize() error
Mobilize initializes the orchestrator and executes all evaluation suites.
func (*EvaluationOrchestrator) PublishManifest ¶ added in v1.31.0
func (v *EvaluationOrchestrator) PublishManifest() (PublishManifest, error)
PublishManifest assembles the publish descriptor from the orchestrator's Publisher + PluginName and its embedded reference catalogs: the coordinate is <Publisher>/<PluginName>, and each evaluated catalog is namespaced under its own owner (metadata.author.id), with id/version/control-ids read from the catalog itself. It fails closed (no manifest) when Publisher or PluginName is unset, when no catalogs are loaded, when a catalog has no version or controls, or when a catalog has no author id and no CatalogNamespaces override (we will not synthesize an owner — claiming an unattributed catalog under any namespace is a false claim). Every case is a "this plugin cannot be published yet" error the author fixes in code or catalog metadata. A plugin RUNS without these; it cannot be PUBLISHED without them.
func (*EvaluationOrchestrator) WriteResults ¶ added in v1.3.0
func (v *EvaluationOrchestrator) WriteResults() error
WriteResults writes the evaluation results to files in the configured output format.
type EvaluationSuite ¶ added in v1.0.0
type EvaluationSuite struct {
Name string `json:"name" yaml:"name"` // Name is the name of the suite
Result gemara.Result `json:"result" yaml:"result"` // Result is Passed if all evaluations in the suite passed
CatalogId string `json:"catalog-id" yaml:"catalog-id"` // CatalogId associates this suite with a catalog
StartTime string `json:"start-time" yaml:"start-time"` // StartTime is the time the plugin started
EndTime string `json:"end-time" yaml:"end-time"` // EndTime is the time the plugin ended
CorruptedState bool `json:"corrupted-state" yaml:"corrupted-state"` // CorruptedState is true if any testSet failed to revert at the end of the evaluation
EvaluationLog gemara.EvaluationLog `json:"control-evaluations" yaml:"control-evaluations"` // EvaluationLog is a slice of evaluations to be executed
// contains filtered or unexported fields
}
EvaluationSuite contains the results of all EvaluationLog executions. Exported fields will be used in the final YAML or JSON output documents.
func (*EvaluationSuite) AddChangeManager ¶ added in v1.7.0
func (e *EvaluationSuite) AddChangeManager(cm *ChangeManager)
AddChangeManager sets up the change manager for the evaluation suite.
func (*EvaluationSuite) Evaluate ¶ added in v1.0.0
func (e *EvaluationSuite) Evaluate(serviceName string) error
Evaluate executes a list of EvaluationLog provided by a Plugin and customized by user config. Name is an arbitrary string that will be used to identify the EvaluationSuite.
func (*EvaluationSuite) GetAssessmentRequirements ¶ added in v1.7.0
func (e *EvaluationSuite) GetAssessmentRequirements() (map[string]*gemara.AssessmentRequirement, error)
GetAssessmentRequirements retrieves all assessment requirements from the catalog.
type LoaderTiming ¶ added in v1.32.0
type LoaderTiming struct {
// Scope is "orchestrator" or "suite:<catalog-id>".
Scope string `json:"scope" yaml:"scope"`
Func string `json:"func" yaml:"func"`
DurationNs int64 `json:"duration-ns" yaml:"duration-ns"`
}
LoaderTiming times one DataLoader invocation.
type PublishManifest ¶ added in v1.31.0
type PublishManifest struct {
// Coordinate is the plugin's grc.store coordinate "<publisher>/<plugin_id>".
Coordinate string `json:"coordinate"`
// License is the plugin's declared publication license, the raw SPDX
// expression from orchestrator.License. It is carried verbatim here; pvtr
// validates and canonicalizes it at publish time (grc-store-protocol/spdx is
// not imported into the plugin binary).
License string `json:"license"`
// Evaluates is the control-catalog linkage, deterministically ordered.
Evaluates []EvaluatesDeclaration `json:"evaluates"`
}
PublishManifest is the machine-readable descriptor `pvtr publish` reads from a built plugin (via the publish-manifest subcommand) in place of CLI flags. The plugin coordinate is Publisher + PluginName. Each evaluated catalog's coordinate is the catalog's OWN owner — metadata.author.id — plus its id, so a plugin that evaluates someone else's catalog links to the real owner instead of falsely claiming it under the plugin's own namespace. A CatalogNamespaces override is available for the rare case where a catalog's author.id does not match the namespace it is published under on grc.store.
type RevertFunc ¶ added in v1.7.0
type RevertFunc func(interface{}) error
RevertFunc is a prepared function to revert a change after it has been applied.
type StepTiming ¶ added in v1.32.0
type StepTiming struct {
ControlId string `json:"control-id" yaml:"control-id"`
RequirementId string `json:"requirement-id" yaml:"requirement-id"`
StepIndex int `json:"step-index" yaml:"step-index"`
Step string `json:"step" yaml:"step"`
Result string `json:"result" yaml:"result"`
DurationNs int64 `json:"duration-ns" yaml:"duration-ns"`
}
StepTiming times one executed assessment step; steps that never ran produce no entry.
type SuiteTiming ¶ added in v1.32.0
type SuiteTiming struct {
CatalogId string `json:"catalog-id" yaml:"catalog-id"`
DurationNs int64 `json:"duration-ns" yaml:"duration-ns"`
Steps []StepTiming `json:"steps" yaml:"steps"`
}
SuiteTiming times one evaluation suite and its executed steps.
type TargetBuilder ¶ added in v1.31.3
TargetBuilder describes the resource a run evaluated, for the target field of the emitted gemara EvaluationLog. It receives the resolved config so it can derive real-world identity from service vars (e.g. "owner/repo" for a GitHub repository plugin). When no builder is registered the target falls back to the service name from the run configuration.
type TestSet ¶
type TestSet func() (result gemara.ControlEvaluation)
TestSet is a function type that returns a control evaluation result.
type TypedAssessmentStep ¶ added in v1.32.0
TypedAssessmentStep is the shape of a payload-typed assessment step: the same contract as gemara.AssessmentStep, but receiving a concrete payload type T instead of an untyped any.
The type parameter S on the registration helpers below is constrained to ~TypedAssessmentStep[T] so a plugin may keep its own named step type (e.g. `type TypedStep func(data.Payload) (...)`) and pass its existing step maps unchanged.