Documentation
¶
Overview ¶
Package platform is the single public extension contract for lark-cli.
External integrators (plugin authors, embedding platforms) only import this package; everything else under internal/ is off-limits.
Plugin lifecycle:
- Plugin - the interface every plugin implements (Name / Version / Capabilities / Install)
- Registrar - what Install receives; the four registration verbs (Observe / Wrap / On / Restrict)
- Capabilities - declared up front: FailurePolicy (FailOpen | FailClosed) and Restricts
- Register - process-wide entry point; plugins call this from init()
Hook surface (what Install hangs off Registrar):
- Observer - side-effect-only callback, panic-safe, runs Before / After RunE
- Wrapper - middleware that can short-circuit via AbortError
- LifecycleHandler - reacts to Startup / Shutdown / etc. (LifecycleEvent + When)
- Selector - chooses which commands a hook applies to (ByDomain / ByWrite / ByReadOnly / ByExactRisk / And / Or / Not, etc.)
- Handler - the inner "run the command" function Wrappers compose around
- Invocation - per-call context passed to handlers (Cmd view + DeniedByPolicy / DenialLayer / DenialPolicySource)
- AbortError - structured short-circuit error from a Wrapper; framework namespaces HookName
Policy surface (what Restrict contributes, also consumable from yaml policy):
- Rule - declarative policy rule (Allow / Deny / MaxRisk / Identities / AllowUnannotated)
- CommandView - read-only command metadata view (Path / Domain / Risk / Identities)
- Risk / Identity - defined string types with closed taxonomies; ParseRisk / ParseIdentity convert raw strings (yaml, cobra annotation) into typed values; r.Rank() gives a comparable rank for the read < write < high-risk-write ordering
- CommandDeniedError - structured error returned to denied callers
Stability: every exported symbol here is part of the contract. Interfaces never gain methods; new host capability surfaces arrive as optional extension interfaces (see EmbeddedSkillsRegistrar). Internal orchestration (staging, validation, RunE wrapping, denial guard) lives under internal/platform, internal/hook and internal/cmdpolicy and is not importable by third parties.
Index ¶
- func Register(p Plugin)
- func ResetForTesting()
- type AbortError
- type Builder
- func (b *Builder) Build() (Plugin, error)
- func (b *Builder) EmbeddedSkills(spec *SkillsOverlay) *Builder
- func (b *Builder) FailClosed() *Builder
- func (b *Builder) FailOpen() *Builder
- func (b *Builder) MustBuild() Plugin
- func (b *Builder) Observer(when When, hookName string, sel Selector, fn Observer) *Builder
- func (b *Builder) On(event LifecycleEvent, hookName string, fn LifecycleHandler) *Builder
- func (b *Builder) RequireCLI(constraint string) *Builder
- func (b *Builder) Restrict(rule *Rule) *Builder
- func (b *Builder) Wrap(hookName string, sel Selector, wrap Wrapper) *Builder
- type Capabilities
- type CommandDeniedError
- type CommandView
- type EmbeddedSkillsRegistrar
- type FailurePolicy
- type Handler
- type Identity
- type Invocation
- type LifecycleContext
- type LifecycleEvent
- type LifecycleHandler
- type Observer
- type Plugin
- type Registrar
- type Risk
- type Rule
- type Selector
- type SkillRefRemap
- type SkillsOverlay
- type When
- type Wrapper
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Register ¶
func Register(p Plugin)
Register adds a plugin to the global registry. Plugins call this from init() (typically through a blank import in the embedder's main).
Register is intentionally tolerant of malformed input: validation happens later in the host's InstallAll phase, where errors can be surfaced through the typed plugin_install envelope. Register itself never panics so that init-time problems do not crash the binary before main has a chance to install its recover-and-envelope logic.
The registry holds plugins in insertion order so InstallAll can process them deterministically.
func ResetForTesting ¶
func ResetForTesting()
ResetForTesting clears the global plugin registry. Exposed for test isolation only — plugin authors and SDK consumers must NOT call this from production code. The function is exported (rather than placed in an internal test-only file) so that `go test ./...` works for every downstream package without an extra build tag.
Tests that exercise plugin registration must defer `t.Cleanup(platform.ResetForTesting)` so subsequent tests start from a clean slate. The helper is NOT goroutine-safe across concurrent `t.Parallel()` tests — the global registry is shared process state.
Types ¶
type AbortError ¶
AbortError is returned by a Wrapper that wants to short-circuit the command chain (instead of calling next). The framework converts it to a typed errs.* error so the JSON envelope carries the structured fields agents expect.
HookName is the framework-namespaced name ("secaudit.approval"); the Registrar adds the plugin-name prefix automatically.
Cause and Detail are optional. Cause lets the consumer use errors.Is/As to find the underlying cause; Detail is serialized into envelope.detail under the "detail" key for agent consumption.
func (*AbortError) Error ¶
func (e *AbortError) Error() string
Error renders a human-readable message; HookName + Reason + Cause are included when present.
func (*AbortError) Unwrap ¶
func (e *AbortError) Unwrap() error
Unwrap enables errors.Is / errors.As to traverse to Cause.
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder is the ergonomic constructor for Plugin. Use it from init():
func init() {
platform.Register(
platform.NewPlugin("audit", "0.1.0").
Observer(platform.After, "log", platform.All(), auditFn).
FailOpen().
MustBuild())
}
The lower-level Plugin interface remains available for cases that need finer control (state on a struct, complex Install logic). The Builder enforces:
- Name format (^[a-z0-9][a-z0-9-]*$)
- hookName format and uniqueness within a plugin
- Restricts ↔ FailClosed consistency (calling Restrict() implies FailClosed, so plugin authors cannot accidentally ship a policy plugin under FailOpen)
- EmbeddedSkills ↔ FailClosed consistency (declaring distribution assets is a build-integrity commitment; falling back to host defaults is never allowed)
- Rule validation via ValidateRule analogues (delegated to internal/cmdpolicy at install time; Builder only fast-fails blatantly bad input)
func NewPlugin ¶
NewPlugin starts a Builder. Name format is validated lazily — errors surface at Build()/MustBuild() time, allowing chained calls without intermediate error handling.
Example (Observer) ¶
ExampleNewPlugin_observer registers an audit Observer that fires after every command, regardless of success or failure.
package main
import (
"context"
"fmt"
"github.com/larksuite/cli/extension/platform"
)
func main() {
p, _ := platform.NewPlugin("audit", "0.1.0").
Observer(platform.After, "log", platform.All(),
func(ctx context.Context, inv platform.Invocation) {
_ = inv.Cmd().Path() // do something useful with the command
}).
FailOpen().
Build()
fmt.Println(p.Name(), p.Version())
}
Output: audit 0.1.0
Example (Restrict) ¶
ExampleNewPlugin_restrict registers a policy plugin that allows only docs/* read commands. Note that Restrict() implicitly sets FailClosed — a policy plugin must abort the binary if it fails to install, not silently disappear.
package main
import (
"fmt"
"github.com/larksuite/cli/extension/platform"
)
func main() {
p, _ := platform.NewPlugin("readonly-docs", "0.1.0").
Restrict(&platform.Rule{
Name: "docs-only",
Allow: []string{"docs/**"},
MaxRisk: platform.RiskRead,
}).
Build()
caps := p.Capabilities()
fmt.Println(caps.Restricts, caps.FailurePolicy == platform.FailClosed)
}
Output: true true
Example (Wrapper) ¶
ExampleNewPlugin_wrapper registers a Wrap that short-circuits any write-class command. The framework converts the returned *AbortError into a structured "hook" envelope; observers still fire on the After stage so audit sees the attempt.
package main
import (
"context"
"fmt"
"github.com/larksuite/cli/extension/platform"
)
func main() {
p, _ := platform.NewPlugin("policy-plugin", "0.1.0").
Wrap("block-writes", platform.ByWrite(),
func(next platform.Handler) platform.Handler {
return func(ctx context.Context, inv platform.Invocation) error {
return &platform.AbortError{
HookName: "block-writes",
Reason: "writes are disabled for this session",
}
}
}).
FailOpen().
Build()
fmt.Println(p.Capabilities().FailurePolicy == platform.FailOpen)
}
Output: true
func (*Builder) Build ¶
Build returns the configured Plugin, or an error if any builder step found a fault. MustBuild panics on the same error.
FailOpen mismatches are checked against the final builder state, not in the chained setters, because FailOpen/FailClosed and the contributing methods may be called in either order.
func (*Builder) EmbeddedSkills ¶ added in v1.0.84
func (b *Builder) EmbeddedSkills(spec *SkillsOverlay) *Builder
EmbeddedSkills contributes a SkillsOverlay (see SkillsOverlay) customizing the CLI's embedded skill content. It implies FailClosed: although skill content is not a command-enforcement boundary, the overlay is a distribution build-integrity declaration. Silently skipping it could republish host defaults that the distribution explicitly removed or replaced.
Calling FailOpen before EmbeddedSkills is allowed; EmbeddedSkills overrides it to FailClosed, matching Restrict. Calling FailOpen afterward leaves an invalid final state that Build rejects. A later FailClosed restores a valid final state. A plugin owns at most one SkillsOverlay, so calling EmbeddedSkills more than once is a build error.
func (*Builder) FailClosed ¶
FailClosed sets Capabilities.FailurePolicy = FailClosed. Implicit when Restrict() or EmbeddedSkills() is called.
func (*Builder) FailOpen ¶
FailOpen sets Capabilities.FailurePolicy = FailOpen. Default when neither FailOpen nor FailClosed is called and neither Restrict nor EmbeddedSkills is used. Build rejects a final FailOpen state after either safety-sensitive contribution.
func (*Builder) MustBuild ¶
MustBuild panics if Build() would return an error. Designed for init():
func init() { platform.Register(platform.NewPlugin(...).MustBuild()) }
A panic in init runs before the framework's recover guard is installed and will crash the binary. That is the intended behaviour: a misconfigured plugin must NOT be silently registered.
func (*Builder) On ¶
func (b *Builder) On(event LifecycleEvent, hookName string, fn LifecycleHandler) *Builder
On registers a LifecycleHandler.
func (*Builder) RequireCLI ¶
RequireCLI sets Capabilities.RequiredCLIVersion (semver constraint, e.g. ">=1.1.0"). Empty string means no requirement.
func (*Builder) Restrict ¶
Restrict contributes a pruning Rule. Calling Restrict implicitly sets Restricts=true and FailurePolicy=FailClosed (the framework requires both to coexist; the builder enforces the pairing so the plugin author cannot accidentally ship a policy plugin under FailOpen). It may be called more than once; each call adds one scoped Rule and the engine OR-combines them.
type Capabilities ¶
type Capabilities struct {
// RequiredCLIVersion is a semver constraint (e.g. ">=1.1.0").
// Plugins that need a specific framework feature should declare
// the minimum version they tested against; the host fails the
// install when the running CLI is older. Empty string means "no
// version requirement".
RequiredCLIVersion string
// Restricts declares whether Install will call r.Restrict(). The
// framework enforces consistency: declaring Restricts=true and
// then NOT calling r.Restrict (or vice versa) aborts the install
// with the `restricts_mismatch` reason_code. This pre-flight
// declaration also lets `config policy show` introspect "which
// plugins are policy plugins" without running them.
Restricts bool
// FailurePolicy decides what happens on install failure. See the
// constants above; the framework requires FailClosed whenever
// Restricts=true or Install contributes EmbeddedSkills.
FailurePolicy FailurePolicy
}
Capabilities declares the plugin's self-description. Plugin.Capabilities MUST be implemented even when every field would be its zero value -- the requirement keeps FailurePolicy / Restricts visible to the author at the moment they write the plugin, preventing the "I just want to add an audit observer" mistake of accidentally shipping a policy plugin with the default FailOpen.
type CommandDeniedError ¶
type CommandDeniedError struct {
Path string
Layer string
PolicySource string
RuleName string
ReasonCode string
Reason string
}
CommandDeniedError is the structured error returned by a denyStub. Every pruned-command execution path -- direct invocation, alias expansion, internal call -- returns this exact type. The dispatcher converts it to a typed errs.* error; the Layer field carries the denial layer for the envelope.
Layer values:
- "strict_mode" -- credential strict-mode rejected the command
- "policy" -- user-layer Rule rejected the command
PolicySource is a free-form identifier such as "plugin:secaudit", "yaml:mywork", or "strict-mode". Reason fields:
- ReasonCode -- closed enum, see tech-doc 5.3 (e.g. write_not_allowed, all_children_denied, identity_not_supported)
- Reason -- human-readable text
func (*CommandDeniedError) Error ¶
func (e *CommandDeniedError) Error() string
Error implements the standard error interface.
type CommandView ¶
type CommandView interface {
// Path is the canonical slash-separated path, rootless ("docs/+update").
Path() string
// Domain returns the business domain ("docs", "im", "") inherited from
// the nearest ancestor with a cmdmeta.domain annotation. Empty string
// when no ancestor declares one.
Domain() string
// Risk returns the static risk level. ok=false signals "no risk_level
// annotation found in the parent chain" (unknown).
Risk() (level Risk, ok bool)
// Identities returns the supported identities. nil signals "no
// supportedIdentities annotation in the parent chain".
Identities() []Identity
// Annotation exposes the raw cobra annotation map for plugins that
// need a tag the framework does not surface.
Annotation(key string) (string, bool)
}
CommandView is the read-only view of a cobra.Command exposed to plugins and the policy engine. *cobra.Command is deliberately NOT reachable through this interface -- a plugin should never mutate the command tree.
View semantics:
The view is a live proxy over the underlying *cobra.Command and its annotation chain. Strict-mode replaces nodes via RemoveCommand+ AddCommand; the replacement stub explicitly carries the original command's annotations and help text forward so audit / compliance observers still see Risk / Identities / Domain after a denial. User-layer policy mutates in place, so its denyStubs preserve the original metadata by construction.
Path() is the canonical slash form ("docs/+fetch"), matching the doublestar glob semantics used by Rule.Allow / Rule.Deny.
Risk() returns ok=false when the command is unannotated. The policy engine treats an unannotated command as implicit deny whenever any Rule without AllowUnannotated=true is registered, so risk-based Selectors never see unannotated commands during normal hook dispatch under that configuration.
type EmbeddedSkillsRegistrar ¶ added in v1.0.84
type EmbeddedSkillsRegistrar interface {
// EmbeddedSkills contributes a SkillsOverlay customizing the CLI's
// embedded skill content (see SkillsOverlay).
EmbeddedSkills(spec *SkillsOverlay)
}
EmbeddedSkillsRegistrar is the optional extension a host registrar implements to accept embedded-skill customization. It is deliberately NOT part of Registrar: every exported symbol in this package is a stability contract, and widening Registrar would break existing third-party implementations (fakes, decorators, custom hosts). A Builder-built plugin type-asserts for this interface at Install time and fails closed when the host lacks it -- a declared customization is never silently dropped.
Skill content has a single owner: a second customizing plugin, a FailOpen declaration, or a SkillsOverlay that cannot compose aborts startup unconditionally. EmbeddedSkills is a distribution build-integrity boundary: silently dropping it could republish host defaults the distribution removed. Removing a skill drops it from skills list/read and from structured framework-owned pointers, but does not disable any command; use Restrict to actually block a command.
type FailurePolicy ¶
type FailurePolicy int
FailurePolicy controls what the framework does when a plugin's install stage fails (Capabilities() panics, Install returns error, etc.).
const ( // FailOpen (default) — log a warning and skip THIS plugin; the rest // of the CLI keeps running. Appropriate for pure-observer plugins // where missing audit data is preferable to a broken CLI. FailOpen FailurePolicy = iota // FailClosed — abort the entire CLI startup. Required for any plugin // that contributes Restrict() (a missing policy plugin = missing // security boundary), EmbeddedSkills() (silently dropping distribution // assets would violate build integrity), or any other safety-sensitive // concern. The Builder sets it automatically for Restrict and // EmbeddedSkills; the host validates hand-written plugins after staging. FailClosed )
type Handler ¶
type Handler func(ctx context.Context, inv Invocation) error
Handler is the inner function shape every Wrapper composes. It IS the "command business logic" from the Wrapper's perspective -- calling next(ctx, inv) inside a Wrapper means "let the command proceed"; returning early without calling next short-circuits.
type Identity ¶
type Identity string
Identity is the identity taxonomy a command supports.
Defined type (not alias) so plugin authors get compile-time + IDE help; raw-string boundaries (yaml, cobra annotation) cross through ParseIdentity.
func ParseIdentity ¶
ParseIdentity converts a raw string into an Identity. Returns ("", nil) for empty input ("not specified"), error for unrecognised values. Matching is strict (case-sensitive, no trim).
type Invocation ¶
type Invocation interface {
// Cmd returns the read-only metadata view of the dispatched command.
Cmd() CommandView
// Args returns a fresh copy of the positional args.
Args() []string
// Started is the wall-clock time the outermost RunE wrapper began.
Started() time.Time
// Err is the error the wrapped handler returned. Populated for
// After observers and the post-next portion of a Wrapper. nil
// before the handler runs.
Err() error
// DeniedByPolicy reports whether the command was rejected by either
// strict-mode or user-layer policy before the chain reached the
// hook. Observers fire even for denied commands (audit case); Wrap
// is physically isolated by the framework so plugins do not need
// to check this themselves before calling next.
DeniedByPolicy() bool
// DenialLayer returns the layer that rejected the command:
//
// "" - not denied
// "strict_mode" - credential strict-mode
// "policy" - user-layer Rule (Plugin.Restrict() or yaml)
DenialLayer() string
// DenialPolicySource returns the specific source identifier
// ("plugin:secaudit", "yaml", "strict-mode"). Empty when not denied.
DenialPolicySource() string
}
Invocation is the per-command data a Wrapper / Observer receives. It is a read-only interface: the framework implementation lives in internal/hook and is never visible to plugins, so plugin code cannot mutate denial state.
The interface is deliberately NOT a context.Context — it is data only, no cancellation. ctx (from the handler signature) carries cancellation / timeout / trace propagation.
Accessor semantics:
- Cmd / Args / Started are populated before the first hook fires
- Err is populated for After observers and the post-next portion of a Wrapper (the value the wrapped handler returned)
- DeniedByPolicy / DenialLayer / DenialPolicySource are populated by the framework's denial guard before any hook runs
type LifecycleContext ¶
type LifecycleContext struct {
Event LifecycleEvent
Err error
}
LifecycleContext is passed to LifecycleHandler. Err is the error from the preceding command (when Event == Shutdown after a failed RunE); otherwise nil.
type LifecycleEvent ¶
type LifecycleEvent int
LifecycleEvent selects the temporal slot for Lifecycle hooks. These are process-level events that fire once per binary execution, not per command. Only Startup and Shutdown are defined: additional bootstrap phases can be added later as a non-breaking addition if a concrete consumer surfaces.
const ( // Startup fires after plugin install has committed; Plugin.On // handlers for Startup are guaranteed to be registered before this // event is emitted (so they can receive it). Startup LifecycleEvent = iota // Shutdown fires once before the process exits. Handler total // execution is bounded by a hard 2s timeout to prevent a // misbehaving handler from holding up exit. Shutdown )
type LifecycleHandler ¶
type LifecycleHandler func(ctx context.Context, lc *LifecycleContext) error
LifecycleHandler runs at one of the process-level LifecycleEvent slots. The handler may use ctx for cancellation; in the Shutdown case the framework supplies a context with a 2-second hard deadline.
type Observer ¶
type Observer func(ctx context.Context, inv Invocation)
Observer is a side-effect-only command hook. No return value, no next-chain control: an Observer can read Invocation but cannot prevent the command from running. Used for audit, metrics, and completion logs. After-stage Observers fire even when the command failed (Invocation.Err() is populated in that case).
type Plugin ¶
type Plugin interface {
Name() string
Version() string
Capabilities() Capabilities
Install(r Registrar) error
}
Plugin is the single contract a third-party / embedding integrator implements to extend lark-cli. Four methods, every one mandatory.
Name must match the grammar ^[a-z0-9][a-z0-9-]*$. The "." character is forbidden so plugin-name + hookName namespacing never produces ambiguous joins.
Capabilities must be implemented even when every field is zero. The requirement is deliberate: it keeps FailurePolicy / Restricts in the author's eyeline.
Install runs once during the Bootstrap pipeline. The plugin uses the supplied Registrar to register hooks and (optionally) a Rule. Errors returned from Install honour the plugin's Capabilities.FailurePolicy (fail-open warns + skips this plugin; fail-closed aborts the CLI).
func RegisteredPlugins ¶
func RegisteredPlugins() []Plugin
RegisteredPlugins returns a snapshot of the global plugin registry. Order matches Register insertion. The host reads this once during InstallAll.
type Registrar ¶
type Registrar interface {
// Observe registers a side-effect-only command hook at the given
// When stage. The selector decides which commands it fires on.
Observe(when When, hookName string, sel Selector, fn Observer)
// Wrap registers a middleware-style command hook. The Wrap chain
// composes left-to-right in registration order; the outermost
// Wrapper runs first.
Wrap(hookName string, sel Selector, w Wrapper)
// On registers a lifecycle handler for the given event.
On(event LifecycleEvent, hookName string, fn LifecycleHandler)
// Restrict contributes a pruning Rule. May be called more than once
// to declare several scoped grants (OR-combined by the engine).
// Plugin rules take precedence over the yaml source; two distinct
// plugins both calling Restrict abort startup.
Restrict(r *Rule)
}
Registrar is the imperative API a plugin uses inside its Install method to wire up hooks and rules. The framework provides a staging implementation that buffers calls and commits them atomically when Install returns nil; failure rolls everything back.
hookName must match the grammar ^[a-z0-9][a-z0-9-]*$ (no dots). The framework prepends the plugin's Name() with a dot so the global hook identifier is "{plugin}.{hook}". A plugin cannot register two hooks with the same name in the same Install call.
Restrict may be called multiple times per plugin; each call adds one scoped Rule (OR-combined by the engine). Two or more DISTINCT plugins contributing Restrict() is a configuration error (the resolver aborts startup).
type Risk ¶
type Risk string
Risk is the three-tier risk taxonomy declared on every command.
A defined type (not an alias of string) so plugin authors get compile-time + IDE candidate help when passing the constants below. Crossing the string boundary (yaml, cobra annotation) goes through ParseRisk so typos surface as `risk_invalid` rather than silently flowing through.
func ParseRisk ¶
ParseRisk converts a raw string (yaml, cobra annotation) into a Risk.
- s == "" → ("", nil) "not specified"
- s 在闭合枚举 → (Risk(s), nil) OK
- s 不在枚举内 → ("", error) invalid
The (absent vs invalid) split mirrors the cmdpolicy engine's risk_not_annotated vs risk_invalid reason codes — callers can treat the "" + nil case as "not specified" without losing the distinction from a typo.
Matching is strict: "Read" / "READ" / " read " are all rejected. annotation is developer code, not user input — strict matching is the typo-catch mechanism, not a normalisation opportunity.
type Rule ¶
type Rule struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
// Allow is a list of doublestar globs (slash-separated paths). An empty
// slice means "no path restriction"; a non-empty slice means "command
// path must match at least one glob".
Allow []string `json:"allow,omitempty"`
// Deny is a list of doublestar globs. A path that matches any Deny glob
// is rejected regardless of Allow.
Deny []string `json:"deny,omitempty"`
// MaxRisk is the highest allowed risk level (inclusive). Empty string
// means "no risk restriction". Comparison uses the closed taxonomy
// read < write < high-risk-write.
MaxRisk Risk `json:"max_risk,omitempty"`
// Identities is the allowed identity whitelist. A command passes when
// the intersection with the command's own supported identities is
// non-empty. Empty slice means "no identity restriction".
Identities []Identity `json:"identities,omitempty"`
// AllowUnannotated controls how commands missing a risk_level
// annotation are handled when this Rule is active.
//
// Default (false, fail-closed): unannotated commands are rejected
// with reason_code=risk_not_annotated. This is the safe default
// — a typo'd or forgotten annotation cannot slip past an
// "agent read-only" rule.
//
// Set to true to opt out during gradual adoption: lark-cli main
// has hundreds of service commands that may not yet carry
// risk_level annotations, and a brand-new policy plugin would
// otherwise lock the binary to nothing.
//
// This flag does NOT affect risk_invalid (typos): a command that
// claims a risk but mis-spells it is always denied, regardless of
// AllowUnannotated. Typo is a code bug, not a migration phase.
//
// No yaml tag: yaml decoding lives in internal/cmdpolicy/yaml so
// platform stays free of a yaml library dependency.
AllowUnannotated bool `json:"allow_unannotated,omitempty"`
}
Rule is the declarative policy rule data structure. yaml files and Plugin.Restrict() both produce the same Rule.
At any moment there is at most one effective SOURCE of rules -- the resolver decides which wins (Plugin > yaml > none); the winning source may contribute several scoped rules. This package only defines the shape; selection lives in internal/cmdpolicy.
The four filter fields are joined by AND. See the engine's Evaluate for the full semantics. JSON tags are used by `config policy show`; yaml parsing lives in internal/cmdpolicy/yaml so the public API does not depend on a yaml library.
type Selector ¶
type Selector func(cmd CommandView) bool
Selector picks the commands a hook fires on. A nil Selector is equivalent to None() -- safer than an "always-match" default because it forces every hook to declare its scope explicitly. Compose selectors with And / Or / Not.
func All ¶
func All() Selector
All matches every command. Use for audit / metrics observers that must run on the whole surface.
func ByCommandPath ¶
ByCommandPath matches against the canonical slash-form path. Patterns are doublestar globs ("docs/+update", "im/*", "**"). Invalid patterns never match; ValidateRule's twin check catches them at the source.
func ByDomain ¶
ByDomain matches a command whose Domain() is one of the supplied names. Commands with unknown (empty-string) Domain never match this selector -- the caller should pair it with a Selector that handles unknown explicitly when that case matters.
func ByExactRisk ¶
ByExactRisk matches commands whose declared risk level is exactly level.
func ByIdentity ¶
ByIdentity matches when the command's supported identities include the supplied id. Unknown identities never match.
func ByWrite ¶
func ByWrite() Selector
ByWrite matches commands whose risk is "write" or "high-risk-write".
type SkillRefRemap ¶ added in v1.0.84
type SkillRefRemap struct {
// contains filtered or unexported fields
}
SkillRefRemap is an immutable mapping between two structured embedded-skill references. Its fields are private so malformed values can only enter via a zero value (which the resolver rejects) or RemapSkillRef.
Use From and To for diagnostics; callers should not parse their values to perform resolution themselves.
func RemapSkillRef ¶ added in v1.0.84
func RemapSkillRef(from, to string) SkillRefRemap
RemapSkillRef maps a canonical "name[/relative/path]" reference to the runtime reference shipped by a distribution. Syntax and target existence are validated when the SkillsOverlay is composed, so it participates in the same build-integrity failure path as Base, Allow, Remove, and Overlay.
func (SkillRefRemap) From ¶ added in v1.0.84
func (m SkillRefRemap) From() string
From returns the canonical source reference.
func (SkillRefRemap) To ¶ added in v1.0.84
func (m SkillRefRemap) To() string
To returns the distribution runtime target reference.
type SkillsOverlay ¶ added in v1.0.84
type SkillsOverlay struct {
// Allow, when non-empty, keeps only these skills (by name) from the
// base tree — the allow-list counterpart of Rule.Allow. Skills the
// CLI adds in future versions stay out of the build until listed
// here, which a Remove-only spec cannot guarantee. A name not
// present in the base aborts startup. Overlay entries are exempt:
// content the integrator explicitly ships needs no allow-listing.
Allow []string
// Remove hides these skills, by name (e.g. "lark-shared"), from the
// base tree; it wins over Allow, mirroring Rule's Deny-over-Allow. A
// name not present in the base aborts startup rather than being
// silently ignored.
Remove []string
// Overlay contributes skills laid over the base: a same-named skill
// replaces the base's entirely, a new name adds one. It is rooted at
// the skill list (entries like "my-skill/SKILL.md"); each top-level
// entry must be a "<name>/" directory containing SKILL.md. Any fs.FS
// works (embed.FS, os.DirFS, fstest.MapFS); embed.FS is not required.
Overlay fs.FS
// Base replaces the host-provided base skill tree instead of layering
// over it. nil keeps whatever base the host wired with
// cmd.SetEmbeddedSkillContent; it does not import the repository
// binary's default into an external wrapper main. Every top-level
// entry must be a valid skill directory containing SKILL.md. Most
// integrators leave Base nil and use Remove/Overlay so unchanged
// host-provided skills need no copy inside the plugin.
Base fs.FS
// ReferenceRemaps maps CLI-authored canonical skill references to the
// runtime names and files this distribution ships. References use the
// same "name[/relative/path]" form accepted by `lark-cli skills read`.
//
// A bare source remaps the whole skill name while preserving the
// referenced relative path:
//
// RemapSkillRef("lark-doc", "acme-docx")
//
// maps both "lark-doc" and
// "lark-doc/references/lark-doc-fetch.md" to the corresponding paths
// under "acme-docx". A source carrying a relative path is an exact
// reference override and wins over the whole-skill mapping:
//
// RemapSkillRef(
// "lark-doc/references/lark-doc-fetch.md",
// "acme-docx/guides/fetch.md",
// )
//
// Remaps affect structured CLI help/affordance references only. They
// do not rewrite prose or links inside embedded Markdown. Every
// explicitly mapped target must exist in the composed tree; otherwise
// startup fails as an invalid SkillsOverlay. An unmapped canonical
// reference whose file is absent is simply unavailable to presenters.
ReferenceRemaps []SkillRefRemap
// contains filtered or unexported fields
}
SkillsOverlay declares how a plugin customizes the CLI's embedded skill content, contributed via Builder.EmbeddedSkills. At most one source may own skill content; two customizing plugins abort startup.
Allow / Remove mirror Rule's Allow / Deny: an allow-list keeps only what it names, a remove-list drops what it names, and Remove wins over Allow. Composition order is fixed: Base (or the host-provided base) -> Allow -> Remove -> Overlay, a same-named skill resolving to Overlay. The repository's root binary provides its base from content_embed.go; an external wrapper main has no implicit CLI default and must call cmd.SetEmbeddedSkillContent before Execute if it relies on that base.
Skills are addressed by exact name (a directory carrying SKILL.md, e.g. "lark-doc"), not by command path and not by glob — the skill list is flat, so misspellings abort startup instead of silently matching nothing. Removing a skill drops its content and framework-generated guidance; it does not disable any command (use Restrict for that). ReferenceRemaps lets a distribution map the CLI's canonical skill references to the runtime names and files it ships.
The top-level skill set and each skill's owning FS are snapshotted when the CLI builds. Later additions or removals of top-level directories do not change the manifest; files within an owned skill directory are read live. Base and Overlay must contain only valid skill directories. A skill may declare hard dependencies under metadata.requires.skills in SKILL.md. Every declared dependency must be present in the final composed manifest; Allow is never widened and Remove is never overridden to satisfy one. A same-named Overlay replacement uses the replacement SKILL.md's dependency metadata, not the base copy's. Declaring this asset composition is a build-integrity commitment: invalid selection, content, ownership, or reference remaps abort the build rather than silently falling back to host defaults.
type When ¶
type When int
When selects the temporal slot for command-level Observer hooks. The framework wraps every command's RunE so both stages always fire, even when RunE itself returns an error (After is failure-safe).
type Wrapper ¶
Wrapper is a middleware-style hook: it receives the rest of the handler chain and returns a wrapped version. The Wrapper decides whether to call next (allow), abstain (deny, return an AbortError), or transform the result. Multiple Wrappers compose left-to-right by registration order; the outermost runs first.
⚠️ IMPORTANT: The factory function `func(next Handler) Handler` is invoked ONCE PER COMMAND DISPATCH, not once at plugin install. This lets the framework recover from a panicking factory and convert it to a structured envelope, but it means any state captured by the outer closure is rebuilt on every command. Long-lived state (HTTP clients, caches, metrics counters) MUST live on the Plugin struct or in package-level variables, never in factory-local captures.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
audit-observer
command
Command audit-observer is a runnable fork of lark-cli that logs every dispatched command to stderr.
|
Command audit-observer is a runnable fork of lark-cli that logs every dispatched command to stderr. |
|
readonly-policy
command
Command readonly-policy is a runnable fork of lark-cli that installs a Rule permitting only docs/* and im/* read commands.
|
Command readonly-policy is a runnable fork of lark-cli that installs a Rule permitting only docs/* and im/* read commands. |