chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:54 +08:00
commit bf9395e022
2349 changed files with 588574 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
// Package platformhost is the bootstrap-time orchestrator that turns the
// global plugin registry (extension/platform.RegisteredPlugins) into:
//
// - a populated internal/hook.Registry (Observer / Wrapper / Lifecycle)
// - a list of cmdpolicy.PluginRule contributions (one per plugin that
// called r.Restrict)
//
// Two key invariants:
//
// - **Atomic install.** A plugin's Install() runs against a staging
// Registrar; only when Install returns nil AND validateSelf passes
// does the host commit the staged hooks/rule. Partial install never
// reaches the live Registry, so a half-loaded plugin cannot leave
// stale Observer / Wrap entries behind.
//
// - **FailurePolicy honoured.** Each plugin declares FailOpen or
// FailClosed. FailOpen plugins are skipped on error (warning to
// stderr); FailClosed plugins abort the whole bootstrap. The
// framework also enforces the Restricts↔FailClosed consistency
// contract (a Restricts=true plugin with FailOpen would be a
// silent security hole and is rejected during install).
//
// The host returns:
//
// - a *hook.Registry ready to install on the command tree
// - a []cmdpolicy.PluginRule for the pruning resolver
// - an error when a FailClosed plugin failed
package internalplatform
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform
import "fmt"
// PluginInstallError is the typed install-time failure. ReasonCode comes
// from the closed enum in the design doc (section 5.3 reason_code
// table). Cause carries the underlying error, if any, so consumers can
// errors.As to inspect it.
type PluginInstallError struct {
PluginName string
ReasonCode string
Reason string
Cause error
}
func (e *PluginInstallError) Error() string {
prefix := fmt.Sprintf("plugin %q (%s)", e.PluginName, e.ReasonCode)
if e.Reason != "" {
prefix += ": " + e.Reason
}
if e.Cause != nil {
prefix += ": " + e.Cause.Error()
}
return prefix
}
func (e *PluginInstallError) Unwrap() error { return e.Cause }
// ReasonCodes for PluginInstallError. The closed enum is referenced by
// the design doc's hard-constraint #15 (reason_code enum closure) and
// drives the JSON envelope's error.detail.reason_code field.
const (
ReasonInvalidPluginName = "invalid_plugin_name"
ReasonPluginNamePanic = "plugin_name_panic"
ReasonInvalidHookName = "invalid_hook_name"
ReasonDuplicateHookName = "duplicate_hook_name"
ReasonInvalidHookRegister = "invalid_hook_registration"
ReasonInvalidRule = "invalid_rule"
ReasonRestrictsMismatch = "restricts_mismatch"
ReasonCapabilityUnmet = "capability_unmet"
ReasonCapabilitiesPanic = "capabilities_panic"
// ReasonInvalidCapability flags a plugin authoring error in
// Capabilities() output -- e.g. a syntactically malformed
// RequiredCLIVersion string. This is distinct from
// ReasonCapabilityUnmet (legitimate version mismatch): an authoring
// bug must NOT be hidden by FailurePolicy=FailOpen, so this code is
// classified as untrusted-config and aborts unconditionally.
ReasonInvalidCapability = "invalid_capability"
ReasonInstallFailed = "install_failed"
ReasonInstallPanic = "install_panic"
ReasonDuplicatePluginName = "duplicate_plugin_name"
ReasonMultipleRestricts = "multiple_restrict_plugins"
)
+344
View File
@@ -0,0 +1,344 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform
import (
"errors"
"fmt"
"io"
"github.com/larksuite/cli/extension/platform"
"github.com/larksuite/cli/internal/cmdpolicy"
"github.com/larksuite/cli/internal/hook"
)
// PluginInfo is the metadata of a successfully-installed plugin,
// captured at install time so diagnostic commands (config plugins show)
// can enumerate plugins without re-calling potentially panic-prone
// plugin methods at display time.
type PluginInfo struct {
Name string
Version string
Capabilities platform.Capabilities
}
// InstallResult is the output of InstallAll. Registry is ready for
// hook.Install; PluginRules feeds into cmdpolicy.Resolve as the
// "plugin contribution" half of the resolver input. Plugins lists
// every plugin that committed successfully (FailOpen-skipped plugins
// are absent), for downstream diagnostics.
type InstallResult struct {
Registry *hook.Registry
PluginRules []cmdpolicy.PluginRule
Plugins []PluginInfo
}
// InstallAll runs every registered plugin through the staging
// Registrar, validates, and commits the survivors. FailOpen plugins
// that fail are skipped with a warning; the first FailClosed failure
// stops the loop and returns the error.
//
// Plugins are processed in registration order so the result is
// deterministic.
//
// errOut receives warnings about FailOpen plugin skips. nil errOut
// means warnings are dropped (useful in tests).
func InstallAll(plugins []platform.Plugin, errOut io.Writer) (*InstallResult, error) {
if errOut == nil {
errOut = io.Discard
}
result := &InstallResult{
Registry: hook.NewRegistry(),
}
// Detect duplicate Plugin.Name. We do this up-front so the error
// surfaces before any Install runs; design hard-constraint #7
// treats this as configuration error (fail-closed regardless of
// individual FailurePolicy).
if err := detectDuplicateNames(plugins); err != nil {
return nil, err
}
for _, p := range plugins {
name, nameErr := safeCallName(p)
if nameErr != nil {
// Fail-closed on bad Name: we don't know the plugin's
// FailurePolicy yet (it's behind Capabilities, and we
// cannot trust Capabilities() before Name() succeeds).
return nil, nameErr
}
if err := installOne(name, p, result); err != nil {
// Some errors must abort regardless of FailurePolicy
// because they imply the plugin's FailurePolicy itself
// cannot be trusted (e.g. the consistency check between
// Restricts and FailClosed failed).
if isUntrustedConfigError(err) {
return nil, err
}
policy := readFailurePolicy(p)
switch policy {
case platform.FailClosed:
return nil, err
default:
fmt.Fprintf(errOut, "warning: plugin %q skipped: %v\n", name, err)
continue
}
}
}
return result, nil
}
// isUntrustedConfigError flags errors where the plugin's declared
// FailurePolicy is itself part of the misconfiguration. For these the
// host MUST abort unconditionally; honouring an FailOpen declaration on
// a misconfigured Restricts plugin would defeat the whole point of the
// consistency check.
func isUntrustedConfigError(err error) bool {
var pi *PluginInstallError
if !errors.As(err, &pi) {
return false
}
return pi.ReasonCode == ReasonRestrictsMismatch ||
pi.ReasonCode == ReasonInvalidPluginName ||
pi.ReasonCode == ReasonPluginNamePanic ||
pi.ReasonCode == ReasonDuplicatePluginName ||
pi.ReasonCode == ReasonInvalidCapability
}
// installOne handles a single plugin: build a staging Registrar, call
// Install, run validateSelf, and on success commit to the live
// Registry / PluginRules. Any error means staged data is discarded.
func installOne(name string, p platform.Plugin, result *InstallResult) error {
caps, capsErr := safeCallCapabilities(p)
if capsErr != nil {
return capsErr
}
// FailurePolicy is a closed enum. An out-of-range value almost
// always means the plugin author shipped FailurePolicy(2)/etc. by
// mistake, and the host's switch on caps.FailurePolicy below would
// silently treat the unknown value as FailOpen — defeating the
// security boundary the policy was meant to express. Reject up
// front with ReasonInvalidCapability (classified as
// untrusted-config, so the abort is unconditional).
if caps.FailurePolicy != platform.FailOpen && caps.FailurePolicy != platform.FailClosed {
return &PluginInstallError{
PluginName: name,
ReasonCode: ReasonInvalidCapability,
Reason: fmt.Sprintf("FailurePolicy=%d is not a recognised value (expected FailOpen or FailClosed)",
caps.FailurePolicy),
}
}
// Strict consistency check: Restricts=true must pair with
// FailClosed (design hard-constraint #6).
if caps.Restricts && caps.FailurePolicy != platform.FailClosed {
return &PluginInstallError{
PluginName: name,
ReasonCode: ReasonRestrictsMismatch,
Reason: "Restricts=true requires FailurePolicy=FailClosed",
}
}
// Version compatibility check. Two distinct failure modes:
//
// 1. Parse error (constraint is malformed, e.g. ">=abc")
// -> ReasonInvalidCapability, classified as untrusted-config
// so the host aborts unconditionally. This is a plugin
// authoring bug; FailurePolicy must NOT mask it.
//
// 2. Legitimate version mismatch (constraint parses fine but
// current CLI does not satisfy it)
// -> ReasonCapabilityUnmet, honours FailurePolicy. A FailOpen
// plugin announcing ">=2.0" against a 1.x CLI is skipped
// with a warning; a FailClosed plugin aborts.
if ok, err := satisfiesRequiredCLIVersion(currentCLIVersion(), caps.RequiredCLIVersion); err != nil {
return &PluginInstallError{
PluginName: name,
ReasonCode: ReasonInvalidCapability,
Reason: err.Error(),
}
} else if !ok {
return &PluginInstallError{
PluginName: name,
ReasonCode: ReasonCapabilityUnmet,
Reason: fmt.Sprintf("CLI version %q does not satisfy plugin requirement %q",
currentCLIVersion(), caps.RequiredCLIVersion),
}
}
staging := newStagingRegistrar(name)
if err := safeCallInstall(p, staging); err != nil {
// Don't double-wrap typed PluginInstallError -- safeCallInstall
// already produces install_panic for recovered panics, and a
// re-wrap would bury the precise reason_code under
// install_failed.
var pi *PluginInstallError
if errors.As(err, &pi) {
return err
}
return &PluginInstallError{
PluginName: name,
ReasonCode: ReasonInstallFailed,
Reason: "Install returned error",
Cause: err,
}
}
if err := staging.validateSelf(caps); err != nil {
return err
}
// Commit staged data atomically.
for _, e := range staging.stagedObservers {
result.Registry.AddObserver(e)
}
for _, e := range staging.stagedWrappers {
result.Registry.AddWrapper(e)
}
for _, e := range staging.stagedLifecycles {
result.Registry.AddLifecycle(e)
}
for _, rule := range staging.rules {
result.PluginRules = append(result.PluginRules, cmdpolicy.PluginRule{
PluginName: name,
Rule: rule,
})
}
// Record the plugin in the inventory. Version is fetched here under
// a recover-wrapped helper so a plugin's Version() panic does not
// abort the install we just committed.
result.Plugins = append(result.Plugins, PluginInfo{
Name: name,
Version: safeCallVersion(p),
Capabilities: caps,
})
return nil
}
// safeCallVersion mirrors safeCallName but for Plugin.Version. Failures
// degrade to the empty string -- Version is informational, not a hard
// contract field, so we never want it to abort installation.
func safeCallVersion(p platform.Plugin) (v string) {
defer func() {
if r := recover(); r != nil {
v = ""
}
}()
return p.Version()
}
// readFailurePolicy reads Capabilities and returns the policy, falling
// back to FailClosed if Capabilities() panics. Defensive default: we
// assume the worst-case (safety-sensitive) when we cannot read the
// declaration.
//
// **Implementation note**: FailClosed must be the value set BEFORE the
// panic-prone call. The zero value of platform.FailurePolicy is
// FailOpen, so a "just return after recover" pattern would silently
// flip the safe-default to FailOpen on panic -- the opposite of what
// the comment claims.
func readFailurePolicy(p platform.Plugin) (policy platform.FailurePolicy) {
policy = platform.FailClosed
defer func() { _ = recover() }()
policy = p.Capabilities().FailurePolicy
return
}
// safeCallName recovers from a panic in Plugin.Name() and surfaces it
// as a typed PluginInstallError. Without recovery, a buggy plugin could
// crash the binary before main has a chance to emit a JSON envelope.
func safeCallName(p platform.Plugin) (string, error) {
var (
name string
err error
)
func() {
defer func() {
if r := recover(); r != nil {
err = &PluginInstallError{
PluginName: "<unknown>",
ReasonCode: ReasonPluginNamePanic,
Reason: fmt.Sprintf("Plugin.Name() panicked: %v", r),
}
}
}()
name = p.Name()
}()
if err != nil {
return "", err
}
if !hookNamePattern.MatchString(name) {
return "", &PluginInstallError{
PluginName: name,
ReasonCode: ReasonInvalidPluginName,
Reason: fmt.Sprintf("Plugin.Name() %q must match ^[a-z0-9][a-z0-9-]*$ (no dots)", name),
}
}
return name, nil
}
// safeCallCapabilities mirrors safeCallName for Capabilities().
func safeCallCapabilities(p platform.Plugin) (caps platform.Capabilities, err error) {
defer func() {
if r := recover(); r != nil {
err = &PluginInstallError{
PluginName: pluginNameOrPlaceholder(p),
ReasonCode: ReasonCapabilitiesPanic,
Reason: fmt.Sprintf("Plugin.Capabilities() panicked: %v", r),
}
}
}()
caps = p.Capabilities()
return caps, nil
}
// safeCallInstall mirrors safeCallName for Install(). Install panics
// become install_panic errors, not crashes.
func safeCallInstall(p platform.Plugin, r platform.Registrar) (err error) {
defer func() {
if rec := recover(); rec != nil {
err = &PluginInstallError{
PluginName: pluginNameOrPlaceholder(p),
ReasonCode: ReasonInstallPanic,
Reason: fmt.Sprintf("Install panicked: %v", rec),
}
}
}()
return p.Install(r)
}
func pluginNameOrPlaceholder(p platform.Plugin) string {
defer func() { _ = recover() }()
if n := p.Name(); n != "" {
return n
}
return "<unknown>"
}
// detectDuplicateNames scans the plugin slice for repeated Plugin.Name
// values. Returns a typed PluginInstallError on the first duplicate so
// the bootstrap aborts.
func detectDuplicateNames(plugins []platform.Plugin) error {
seen := map[string]bool{}
for _, p := range plugins {
name, err := safeCallName(p)
if err != nil {
// Don't double-report: let installOne handle naming
// errors per-plugin so we get the same code path.
continue
}
if seen[name] {
return &PluginInstallError{
PluginName: name,
ReasonCode: ReasonDuplicatePluginName,
Reason: fmt.Sprintf("duplicate Plugin.Name() %q across plugins", name),
}
}
seen[name] = true
}
return nil
}
+433
View File
@@ -0,0 +1,433 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform_test
import (
"bytes"
"context"
"errors"
"strings"
"testing"
"github.com/larksuite/cli/extension/platform"
internalplatform "github.com/larksuite/cli/internal/platform"
)
// happyPlugin is a textbook plugin: declares Capabilities, calls a few
// Registrar methods, returns nil. The install pipeline must accept it.
type happyPlugin struct{ name string }
func (p happyPlugin) Name() string { return p.name }
func (p happyPlugin) Version() string { return "1.0.0" }
func (p happyPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
FailurePolicy: platform.FailOpen,
}
}
func (p happyPlugin) Install(r platform.Registrar) error {
r.Observe(platform.Before, "audit-pre", platform.All(),
func(context.Context, platform.Invocation) {})
r.Wrap("policy", platform.All(),
func(next platform.Handler) platform.Handler {
return func(ctx context.Context, inv platform.Invocation) error {
return next(ctx, inv)
}
})
r.On(platform.Shutdown, "flush",
func(context.Context, *platform.LifecycleContext) error { return nil })
return nil
}
func TestInstallAll_happyPlugin(t *testing.T) {
result, err := internalplatform.InstallAll([]platform.Plugin{happyPlugin{name: "audit"}}, nil)
if err != nil {
t.Fatalf("InstallAll: %v", err)
}
if result.Registry == nil {
t.Fatalf("registry should be populated")
}
if len(result.PluginRules) != 0 {
t.Errorf("happy plugin did not call Restrict; rules should be empty")
}
// Cross-check: observers, wrappers, lifecycles got staged through to the live Registry.
if len(result.Registry.MatchingObservers(fakeView{}, platform.Before)) != 1 {
t.Errorf("Before observer not committed")
}
if len(result.Registry.MatchingWrappers(fakeView{})) != 1 {
t.Errorf("Wrapper not committed")
}
if len(result.Registry.LifecycleHandlers(platform.Shutdown)) != 1 {
t.Errorf("Shutdown lifecycle not committed")
}
}
// fakeView satisfies platform.CommandView for selector lookups in the
// platformhost tests; All() matches everything so the type can stay
// trivial.
type fakeView struct{}
func (fakeView) Path() string { return "" }
func (fakeView) Domain() string { return "" }
func (fakeView) Risk() (platform.Risk, bool) { return "", false }
func (fakeView) Identities() []platform.Identity { return nil }
func (fakeView) Annotation(string) (string, bool) { return "", false }
// A FailClosed plugin whose Install returns an error must abort
// InstallAll. Design hard-constraint #6.
type failClosedPlugin struct{}
func (failClosedPlugin) Name() string { return "secaudit" }
func (failClosedPlugin) Version() string { return "1.0.0" }
func (failClosedPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
FailurePolicy: platform.FailClosed,
}
}
func (failClosedPlugin) Install(platform.Registrar) error {
return errors.New("upstream unreachable")
}
func TestInstallAll_failClosedAborts(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{failClosedPlugin{}}, nil)
if err == nil {
t.Fatalf("FailClosed install error should abort")
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) {
t.Fatalf("error must be *PluginInstallError, got %T", err)
}
if pi.ReasonCode != internalplatform.ReasonInstallFailed {
t.Errorf("ReasonCode = %q, want install_failed", pi.ReasonCode)
}
}
// FailOpen install failure logs a warning and skips this plugin; other
// plugins still get installed.
type failOpenPlugin struct{}
func (failOpenPlugin) Name() string { return "audit-broken" }
func (failOpenPlugin) Version() string { return "1.0.0" }
func (failOpenPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{FailurePolicy: platform.FailOpen}
}
func (failOpenPlugin) Install(platform.Registrar) error {
return errors.New("could not connect")
}
func TestInstallAll_failOpenSkips(t *testing.T) {
var buf bytes.Buffer
plugins := []platform.Plugin{
failOpenPlugin{},
happyPlugin{name: "audit"},
}
result, err := internalplatform.InstallAll(plugins, &buf)
if err != nil {
t.Fatalf("FailOpen failure must not abort, got %v", err)
}
if !strings.Contains(buf.String(), "audit-broken") {
t.Errorf("FailOpen warning should mention plugin name, got %q", buf.String())
}
// Second plugin's observer should be present.
if len(result.Registry.MatchingObservers(fakeView{}, platform.Before)) != 1 {
t.Errorf("happy plugin's observer should still be installed after first plugin skipped")
}
}
// Restricts=true with FailOpen is a configuration error: a policy
// plugin that silently disappears under FailOpen would erase the
// security boundary. The host must reject this combo BEFORE Install
// runs.
type misconfiguredRestrictPlugin struct{}
func (misconfiguredRestrictPlugin) Name() string { return "secaudit" }
func (misconfiguredRestrictPlugin) Version() string { return "1.0.0" }
func (misconfiguredRestrictPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
Restricts: true, // policy plugin
FailurePolicy: platform.FailOpen, // contradicts safety contract
}
}
func (misconfiguredRestrictPlugin) Install(platform.Registrar) error { return nil }
func TestInstallAll_restrictsRequiresFailClosed(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{misconfiguredRestrictPlugin{}}, nil)
if err == nil {
t.Fatalf("Restricts+FailOpen must abort")
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) || pi.ReasonCode != internalplatform.ReasonRestrictsMismatch {
t.Fatalf("ReasonCode = %v, want restricts_mismatch", pi)
}
}
// Restricts=true but Install didn't call r.Restrict -> mismatch.
type lyingRestrictPlugin struct{}
func (lyingRestrictPlugin) Name() string { return "p" }
func (lyingRestrictPlugin) Version() string { return "1.0.0" }
func (lyingRestrictPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
Restricts: true,
FailurePolicy: platform.FailClosed,
}
}
func (lyingRestrictPlugin) Install(platform.Registrar) error {
// Forgot to call r.Restrict.
return nil
}
func TestInstallAll_restrictsDeclaredButNotCalled(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{lyingRestrictPlugin{}}, nil)
if err == nil {
t.Fatalf("missing Restrict call when declared must fail")
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) || pi.ReasonCode != internalplatform.ReasonRestrictsMismatch {
t.Fatalf("ReasonCode = %v, want restricts_mismatch", pi)
}
}
// Plugin that panics inside Install must NOT crash the binary -- the
// host recovers and converts the panic into a typed install_panic.
type panicInstallPlugin struct{}
func (panicInstallPlugin) Name() string { return "panicker" }
func (panicInstallPlugin) Version() string { return "1.0.0" }
func (panicInstallPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{FailurePolicy: platform.FailClosed}
}
func (panicInstallPlugin) Install(platform.Registrar) error {
panic("boom")
}
func TestInstallAll_installPanicRecovered(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{panicInstallPlugin{}}, nil)
if err == nil {
t.Fatalf("Install panic should surface as error")
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) || pi.ReasonCode != internalplatform.ReasonInstallPanic {
t.Fatalf("ReasonCode = %v, want install_panic", pi)
}
}
// Two plugins with the same Name must abort before any Install runs.
func TestInstallAll_duplicatePluginName(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{
happyPlugin{name: "audit"},
happyPlugin{name: "audit"},
}, nil)
if err == nil {
t.Fatalf("duplicate Plugin.Name must abort")
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) || pi.ReasonCode != internalplatform.ReasonDuplicatePluginName {
t.Fatalf("ReasonCode = %v, want duplicate_plugin_name", pi)
}
}
// Plugin with an invalid Name (contains "." or starts with a hyphen)
// must abort with invalid_plugin_name. The dot ban is critical -- the
// "{plugin}.{hook}" namespace join would become ambiguous if dots were
// allowed inside Plugin.Name().
type badNamePlugin struct{ n string }
func (p badNamePlugin) Name() string { return p.n }
func (p badNamePlugin) Version() string { return "1.0.0" }
func (p badNamePlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{FailurePolicy: platform.FailClosed}
}
func (p badNamePlugin) Install(platform.Registrar) error { return nil }
func TestInstallAll_invalidPluginName(t *testing.T) {
cases := []string{"with.dot", "", "-leading-hyphen", "UPPER"}
for _, name := range cases {
t.Run(name, func(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{badNamePlugin{n: name}}, nil)
if err == nil {
t.Fatalf("invalid name %q should abort", name)
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) || pi.ReasonCode != internalplatform.ReasonInvalidPluginName {
t.Fatalf("ReasonCode = %v, want invalid_plugin_name", pi)
}
})
}
}
// Plugin's Install registers two hooks with the same name -- the
// staging Registrar rejects the second one with duplicate_hook_name.
type duplicateHookPlugin struct{}
func (duplicateHookPlugin) Name() string { return "dup" }
func (duplicateHookPlugin) Version() string { return "1.0.0" }
func (duplicateHookPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{FailurePolicy: platform.FailClosed}
}
func (duplicateHookPlugin) Install(r platform.Registrar) error {
r.Observe(platform.Before, "x", platform.All(), func(context.Context, platform.Invocation) {})
r.Observe(platform.After, "x", platform.All(), func(context.Context, platform.Invocation) {})
return nil
}
func TestInstallAll_duplicateHookName(t *testing.T) {
_, err := internalplatform.InstallAll([]platform.Plugin{duplicateHookPlugin{}}, nil)
if err == nil {
t.Fatalf("duplicate hookName within same plugin must abort")
}
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) || pi.ReasonCode != internalplatform.ReasonDuplicateHookName {
t.Fatalf("ReasonCode = %v, want duplicate_hook_name", pi)
}
}
// Restrict contributes a rule to result.PluginRules so the pruning
// resolver can pick it up. Exercise the full path.
type restrictPlugin struct{ rule *platform.Rule }
func (p restrictPlugin) Name() string { return "secaudit" }
func (p restrictPlugin) Version() string { return "1.0.0" }
func (p restrictPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
Restricts: true,
FailurePolicy: platform.FailClosed,
}
}
func (p restrictPlugin) Install(r platform.Registrar) error {
r.Restrict(p.rule)
return nil
}
func TestInstallAll_restrictPropagatesRule(t *testing.T) {
rule := &platform.Rule{
Name: "secaudit-policy",
MaxRisk: "read",
Allow: []string{"docs/**"},
Deny: []string{"docs/+delete-doc"},
Identities: []platform.Identity{"bot"},
}
result, err := internalplatform.InstallAll([]platform.Plugin{restrictPlugin{rule: rule}}, nil)
if err != nil {
t.Fatalf("InstallAll: %v", err)
}
if len(result.PluginRules) != 1 {
t.Fatalf("expected 1 plugin rule, got %d", len(result.PluginRules))
}
stored := result.PluginRules[0].Rule
if stored == nil {
t.Fatalf("stored rule is nil")
}
// stagingRegistrar.Restrict defensively clones the plugin-supplied
// rule so a misbehaving plugin can't mutate it after Install
// returns. The clone must carry identical contents but live on a
// distinct pointer.
if stored == rule {
t.Errorf("stored rule should be a clone, got identical pointer")
}
if stored.Name != rule.Name || stored.MaxRisk != rule.MaxRisk {
t.Errorf("stored rule lost data: %+v", stored)
}
if got, want := len(stored.Allow), len(rule.Allow); got != want {
t.Errorf("stored Allow len = %d, want %d", got, want)
}
// Verify the clone is actually isolated: mutating the plugin's
// rule after install must not change the stored one.
rule.Allow[0] = "evil/**"
rule.Deny = append(rule.Deny, "extra/**")
if stored.Allow[0] == "evil/**" {
t.Errorf("Allow slice aliased plugin storage")
}
if len(stored.Deny) != 1 {
t.Errorf("Deny slice aliased plugin storage: %v", stored.Deny)
}
if result.PluginRules[0].PluginName != "secaudit" {
t.Errorf("PluginName = %q", result.PluginRules[0].PluginName)
}
}
// Atomic install: a plugin whose validation fails AFTER it registered
// some hooks must NOT leak those hooks into the live registry. The
// staging buffer is the atomicity boundary.
type partiallyRegisterThenFailPlugin struct{}
func (partiallyRegisterThenFailPlugin) Name() string { return "partial" }
func (partiallyRegisterThenFailPlugin) Version() string { return "1.0.0" }
func (partiallyRegisterThenFailPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
Restricts: true, // declares Restrict but won't call it
FailurePolicy: platform.FailClosed,
}
}
func (partiallyRegisterThenFailPlugin) Install(r platform.Registrar) error {
r.Observe(platform.Before, "would-leak", platform.All(),
func(context.Context, platform.Invocation) {})
// validateSelf will fail because Restricts=true but Restrict
// was not called -- this is the atomic-rollback case.
return nil
}
func TestInstallAll_atomicRollback(t *testing.T) {
_, err := internalplatform.InstallAll(
[]platform.Plugin{partiallyRegisterThenFailPlugin{}, happyPlugin{name: "audit"}},
nil,
)
if err == nil {
t.Fatalf("partial plugin should abort (FailClosed)")
}
// We cannot check Registry contents here because InstallAll
// returns nil on failure; the rollback invariant is "nothing the
// failing plugin staged ever reached a live Registry", which is
// proven by the fact that we got nil back. A weaker but useful
// check: even if we passed a happy second plugin, the loop must
// have stopped at the first FailClosed failure.
var pi *internalplatform.PluginInstallError
if !errors.As(err, &pi) {
t.Fatalf("error must be *PluginInstallError, got %T", err)
}
}
// multiRestrictPlugin calls r.Restrict twice -- the multi-rule case. A
// single plugin may declare several scoped grants; both must be collected
// into PluginRules under the same plugin name, in registration order.
type multiRestrictPlugin struct{}
func (multiRestrictPlugin) Name() string { return "secaudit" }
func (multiRestrictPlugin) Version() string { return "1.0.0" }
func (multiRestrictPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
Restricts: true,
FailurePolicy: platform.FailClosed,
}
}
func (multiRestrictPlugin) Install(r platform.Registrar) error {
r.Restrict(&platform.Rule{Name: "docs-ro", Allow: []string{"docs/**"}, MaxRisk: platform.RiskRead})
r.Restrict(&platform.Rule{Name: "im-rw", Allow: []string{"im/**"}, MaxRisk: platform.RiskWrite})
return nil
}
// A single plugin calling Restrict more than once is valid (multi-rule
// support): both rules are collected, in order, under the one plugin name.
// This pins the behaviour change from the old "Restrict at most once"
// double_restrict error.
func TestInstallAll_multipleRestrictPerPlugin(t *testing.T) {
result, err := internalplatform.InstallAll([]platform.Plugin{multiRestrictPlugin{}}, nil)
if err != nil {
t.Fatalf("multiple Restrict per plugin must succeed, got %v", err)
}
if len(result.PluginRules) != 2 {
t.Fatalf("PluginRules = %d, want 2", len(result.PluginRules))
}
for _, pr := range result.PluginRules {
if pr.PluginName != "secaudit" {
t.Errorf("PluginName = %q, want secaudit", pr.PluginName)
}
}
if result.PluginRules[0].Rule.Name != "docs-ro" || result.PluginRules[1].Rule.Name != "im-rw" {
t.Errorf("rules out of order: %q, %q",
result.PluginRules[0].Rule.Name, result.PluginRules[1].Rule.Name)
}
}
+272
View File
@@ -0,0 +1,272 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform
import (
"strings"
"sync"
"github.com/larksuite/cli/extension/platform"
"github.com/larksuite/cli/internal/hook"
)
// HookEntry is the displayable form of one registered hook.
type HookEntry struct {
Name string `json:"name"`
When string `json:"when,omitempty"` // observers only
Event string `json:"event,omitempty"` // lifecycle only
}
// PluginEntry collects everything one plugin contributed.
type PluginEntry struct {
Name string
Version string
Capabilities CapabilitiesView
// Rules holds the plugin's Restrict contributions, one per r.Restrict
// call (a plugin may declare several scoped rules). Empty when the
// plugin did not call r.Restrict.
Rules []*RuleView
Observers []HookEntry
Wrappers []HookEntry
Lifecycles []HookEntry
}
// CapabilitiesView mirrors platform.Capabilities for display. We keep a
// separate struct so the JSON shape stays under our control and does
// not drift with extension/platform.
type CapabilitiesView struct {
Restricts bool `json:"restricts"`
FailurePolicy string `json:"failure_policy"`
RequiredCLIVersion string `json:"required_cli_version,omitempty"`
}
// NewCapabilitiesView converts a platform.Capabilities value into the
// display struct.
func NewCapabilitiesView(c platform.Capabilities) CapabilitiesView {
return CapabilitiesView{
Restricts: c.Restricts,
FailurePolicy: failurePolicyLabel(c.FailurePolicy),
RequiredCLIVersion: c.RequiredCLIVersion,
}
}
func failurePolicyLabel(p platform.FailurePolicy) string {
switch p {
case platform.FailOpen:
return "FailOpen"
case platform.FailClosed:
return "FailClosed"
}
return ""
}
// RuleView is the displayable form of a Plugin.Restrict contribution.
type RuleView struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Allow []string `json:"allow"`
Deny []string `json:"deny"`
MaxRisk string `json:"max_risk"`
Identities []string `json:"identities"`
AllowUnannotated bool `json:"allow_unannotated"`
}
// Inventory is the full snapshot.
type Inventory struct {
Plugins []PluginEntry
}
// PluginInventorySource is the minimum slice of PluginInfo BuildInventory needs.
type PluginInventorySource struct {
Name string
Version string
Capabilities platform.Capabilities
}
// RuleInventorySource is the minimum slice of cmdpolicy.PluginRule
// BuildInventory needs. Kept as plain strings to avoid an import
// cycle with cmdpolicy (the caller converts platform.Risk / Identity
// to string at the boundary).
type RuleInventorySource struct {
PluginName string
Allow []string
Deny []string
MaxRisk string
Identities []string
RuleName string
Desc string
AllowUnannotated bool
}
// BuildInventory assembles an Inventory from the parts produced by
// InstallAll: the plugin metadata list, the hook registry (may be nil
// when no hooks were registered), and the plugin rules.
//
// Hooks are attributed to plugins by the namespaced name convention:
// each entry's Name starts with "<plugin>.", and we group by the
// leading segment up to the first dot.
func BuildInventory(plugins []PluginInventorySource, registry *hook.Registry, rules []RuleInventorySource) *Inventory {
byPlugin := make(map[string]*PluginEntry, len(plugins))
out := &Inventory{Plugins: make([]PluginEntry, 0, len(plugins))}
for _, p := range plugins {
entry := PluginEntry{
Name: p.Name,
Version: p.Version,
Capabilities: NewCapabilitiesView(p.Capabilities),
}
out.Plugins = append(out.Plugins, entry)
}
for i := range out.Plugins {
byPlugin[out.Plugins[i].Name] = &out.Plugins[i]
}
if registry != nil {
for _, e := range registry.Observers() {
if entry := byPlugin[ownerOf(e.Name)]; entry != nil {
entry.Observers = append(entry.Observers, HookEntry{
Name: e.Name,
When: whenLabel(e.When),
})
}
}
for _, e := range registry.Wrappers() {
if entry := byPlugin[ownerOf(e.Name)]; entry != nil {
entry.Wrappers = append(entry.Wrappers, HookEntry{
Name: e.Name,
})
}
}
for _, e := range registry.Lifecycles() {
if entry := byPlugin[ownerOf(e.Name)]; entry != nil {
entry.Lifecycles = append(entry.Lifecycles, HookEntry{
Name: e.Name,
Event: eventLabel(e.Event),
})
}
}
}
for _, r := range rules {
if entry := byPlugin[r.PluginName]; entry != nil {
entry.Rules = append(entry.Rules, &RuleView{
Name: r.RuleName,
Description: r.Desc,
Allow: append([]string(nil), r.Allow...),
Deny: append([]string(nil), r.Deny...),
MaxRisk: r.MaxRisk,
Identities: append([]string(nil), r.Identities...),
AllowUnannotated: r.AllowUnannotated,
})
}
}
return out
}
// ownerOf extracts the plugin name from a namespaced hook name. The
// platform forbids "." in plugin names, so the first dot is always the
// namespace separator. Names without a dot are returned as-is.
func ownerOf(hookName string) string {
if i := strings.IndexByte(hookName, '.'); i >= 0 {
return hookName[:i]
}
return hookName
}
func whenLabel(w platform.When) string {
switch w {
case platform.Before:
return "Before"
case platform.After:
return "After"
}
return ""
}
func eventLabel(e platform.LifecycleEvent) string {
switch e {
case platform.Startup:
return "Startup"
case platform.Shutdown:
return "Shutdown"
}
return ""
}
// --- Active inventory storage (process-global) ---
var (
inventoryMu sync.RWMutex
activeInventory *Inventory
)
// SetActiveInventory records the inventory built at bootstrap. Called
// once from cmd/policy.go after install + wireHooks complete.
//
// A deep copy is taken so the snapshot is immune to later mutations of
// the input by the caller (or by any other goroutine reading the same
// PluginEntry slice). Without deep-copy, the shallow `cp := *inv`
// previously still aliased Plugins / observer / wrapper / lifecycle
// slices and the embedded RuleView's slice fields.
func SetActiveInventory(inv *Inventory) {
inventoryMu.Lock()
defer inventoryMu.Unlock()
if inv == nil {
activeInventory = nil
return
}
activeInventory = cloneInventory(inv)
}
// GetActiveInventory returns a deep copy of the inventory, or nil if
// bootstrap has not finished. Same reasoning as SetActiveInventory:
// returning a shallow copy would let callers reach into the stored
// global through any of the embedded slices.
func GetActiveInventory() *Inventory {
inventoryMu.RLock()
defer inventoryMu.RUnlock()
if activeInventory == nil {
return nil
}
return cloneInventory(activeInventory)
}
// cloneInventory deep-copies every level the snapshot exposes:
// top-level struct, Plugins slice, each PluginEntry's hook slices, and
// the rule's slice fields. The hook entries themselves are value types
// so the slice copy already disjoints them.
func cloneInventory(in *Inventory) *Inventory {
if in == nil {
return nil
}
out := &Inventory{
Plugins: make([]PluginEntry, len(in.Plugins)),
}
for i, p := range in.Plugins {
entry := PluginEntry{
Name: p.Name,
Version: p.Version,
Capabilities: p.Capabilities,
}
if p.Rules != nil {
entry.Rules = make([]*RuleView, len(p.Rules))
for j, r := range p.Rules {
if r == nil {
continue
}
rv := *r
rv.Allow = append([]string(nil), r.Allow...)
rv.Deny = append([]string(nil), r.Deny...)
rv.Identities = append([]string(nil), r.Identities...)
entry.Rules[j] = &rv
}
}
entry.Observers = append([]HookEntry(nil), p.Observers...)
entry.Wrappers = append([]HookEntry(nil), p.Wrappers...)
entry.Lifecycles = append([]HookEntry(nil), p.Lifecycles...)
out.Plugins[i] = entry
}
return out
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform_test
import (
"context"
"testing"
"github.com/larksuite/cli/extension/platform"
"github.com/larksuite/cli/internal/hook"
internalplatform "github.com/larksuite/cli/internal/platform"
)
func TestBuildInventory_groupsByPluginName(t *testing.T) {
plugins := []internalplatform.PluginInventorySource{
{Name: "a", Version: "1.0", Capabilities: platform.Capabilities{
Restricts: true, FailurePolicy: platform.FailClosed,
}},
{Name: "b", Version: "2.0"},
}
r := hook.NewRegistry()
obs := func(context.Context, platform.Invocation) {}
wrap := func(next platform.Handler) platform.Handler { return next }
lc := func(context.Context, *platform.LifecycleContext) error { return nil }
r.AddObserver(hook.ObserverEntry{Name: "a.pre", When: platform.Before, Selector: platform.All(), Fn: obs})
r.AddObserver(hook.ObserverEntry{Name: "a.post", When: platform.After, Selector: platform.All(), Fn: obs})
r.AddObserver(hook.ObserverEntry{Name: "b.audit", When: platform.Before, Selector: platform.All(), Fn: obs})
r.AddWrapper(hook.WrapperEntry{Name: "a.approval", Selector: platform.All(), Fn: wrap})
r.AddLifecycle(hook.LifecycleEntry{Name: "a.boot", Event: platform.Startup, Fn: lc})
r.AddLifecycle(hook.LifecycleEntry{Name: "b.bye", Event: platform.Shutdown, Fn: lc})
rules := []internalplatform.RuleInventorySource{
{PluginName: "a", RuleName: "a-rule", Allow: []string{"docs/**"}, MaxRisk: "read"},
}
inv := internalplatform.BuildInventory(plugins, r, rules)
if got := len(inv.Plugins); got != 2 {
t.Fatalf("Plugins len = %d, want 2", got)
}
a := findPlugin(inv, "a")
b := findPlugin(inv, "b")
if a == nil || b == nil {
t.Fatalf("missing entries: a=%v b=%v", a, b)
}
if got := len(a.Observers); got != 2 {
t.Errorf("a.Observers = %d, want 2", got)
}
if got := len(a.Wrappers); got != 1 {
t.Errorf("a.Wrappers = %d, want 1", got)
}
if got := len(a.Lifecycles); got != 1 {
t.Errorf("a.Lifecycles = %d, want 1", got)
}
if len(a.Rules) != 1 || a.Rules[0].Name != "a-rule" {
t.Errorf("a.Rules = %+v, want single rule name a-rule", a.Rules)
}
if a.Capabilities.FailurePolicy != "FailClosed" {
t.Errorf("a.Capabilities.FailurePolicy = %q, want FailClosed", a.Capabilities.FailurePolicy)
}
if got := len(b.Observers); got != 1 {
t.Errorf("b.Observers = %d, want 1 (only b.audit)", got)
}
if len(b.Rules) != 0 {
t.Errorf("b.Rules = %+v, want empty (b did not call Restrict)", b.Rules)
}
if b.Capabilities.FailurePolicy != "FailOpen" {
t.Errorf("b.Capabilities.FailurePolicy = %q, want FailOpen (zero value)", b.Capabilities.FailurePolicy)
}
}
// A plugin contributing several rules (same PluginName, multiple
// RuleInventorySource entries) must surface ALL of them under Rules, in
// order -- not silently overwrite down to the last one. Pins the
// multi-rule inventory fix.
func TestBuildInventory_multipleRulesPerPlugin(t *testing.T) {
plugins := []internalplatform.PluginInventorySource{
{Name: "a", Version: "1.0", Capabilities: platform.Capabilities{
Restricts: true, FailurePolicy: platform.FailClosed,
}},
}
rules := []internalplatform.RuleInventorySource{
{PluginName: "a", RuleName: "docs-ro", Allow: []string{"docs/**"}, MaxRisk: "read"},
{PluginName: "a", RuleName: "im-rw", Allow: []string{"im/**"}, MaxRisk: "write"},
}
inv := internalplatform.BuildInventory(plugins, nil, rules)
a := findPlugin(inv, "a")
if a == nil {
t.Fatalf("missing entry a")
}
if len(a.Rules) != 2 {
t.Fatalf("a.Rules = %d, want 2 (both rules preserved, no overwrite)", len(a.Rules))
}
if a.Rules[0].Name != "docs-ro" || a.Rules[1].Name != "im-rw" {
t.Errorf("rules out of order: %q, %q", a.Rules[0].Name, a.Rules[1].Name)
}
}
func TestBuildInventory_empty(t *testing.T) {
inv := internalplatform.BuildInventory(nil, nil, nil)
if got := len(inv.Plugins); got != 0 {
t.Errorf("Plugins len = %d, want 0", got)
}
}
func findPlugin(inv *internalplatform.Inventory, name string) *internalplatform.PluginEntry {
for i := range inv.Plugins {
if inv.Plugins[i].Name == name {
return &inv.Plugins[i]
}
}
return nil
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform
import (
"fmt"
"regexp"
"github.com/larksuite/cli/extension/platform"
"github.com/larksuite/cli/internal/hook"
)
// hookNamePattern is the grammar both Plugin.Name() and hookName must
// match -- design hard-constraint #9. The "." character is forbidden so
// the namespace join "{plugin}.{hook}" is unambiguous.
var hookNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
// stagingRegistrar buffers every Registrar call so the platformhost can
// commit them atomically (or discard them all) once Install returns.
//
// All validation happens here at staging time -- bad hookName, nil
// handler, duplicate names, etc. produce typed errors that surface in
// validateSelf and are translated into PluginInstallError by the host
// loop.
type stagingRegistrar struct {
pluginName string
stagedObservers []hook.ObserverEntry
stagedWrappers []hook.WrapperEntry
stagedLifecycles []hook.LifecycleEntry
// rules holds the staged Restrict contributions, captured for the
// host to feed into the resolver later. Empty means the plugin did
// not call r.Restrict. A plugin may call Restrict more than once;
// each call adds one scoped Rule (OR-combined by the engine).
rules []*platform.Rule
// actuallyRestricted records whether r.Restrict was called at all
// (even Restrict(nil)), so the Restricts-vs-actual consistency check
// can detect the call.
actuallyRestricted bool
// seenHookNames detects duplicate hookName within this plugin's
// Install call.
seenHookNames map[string]bool
// stagingErrs accumulates per-call validation errors. A single
// Install can violate the grammar multiple times; collecting all
// of them lets diagnostic output show the full picture.
stagingErrs []stagingErr
}
// stagingErr is the per-call buffered validation failure.
type stagingErr struct {
reasonCode string
message string
}
func newStagingRegistrar(pluginName string) *stagingRegistrar {
return &stagingRegistrar{
pluginName: pluginName,
seenHookNames: map[string]bool{},
}
}
// --- Registrar interface ---
func (r *stagingRegistrar) Observe(when platform.When, name string, sel platform.Selector, fn platform.Observer) {
if !r.validateName(name) {
return
}
if !r.validateNonNilSelector(name, sel) {
return
}
if fn == nil {
r.bufferErr(ReasonInvalidHookRegister, fmt.Sprintf("observe %q: handler is nil", name))
return
}
if !isValidWhen(when) {
r.bufferErr(ReasonInvalidHookRegister, fmt.Sprintf("observe %q: invalid When value %d", name, when))
return
}
r.stagedObservers = append(r.stagedObservers, hook.ObserverEntry{
Name: r.namespaced(name),
When: when,
Selector: sel,
Fn: fn,
})
}
func (r *stagingRegistrar) Wrap(name string, sel platform.Selector, w platform.Wrapper) {
if !r.validateName(name) {
return
}
if !r.validateNonNilSelector(name, sel) {
return
}
if w == nil {
r.bufferErr(ReasonInvalidHookRegister, fmt.Sprintf("wrap %q: handler is nil", name))
return
}
r.stagedWrappers = append(r.stagedWrappers, hook.WrapperEntry{
Name: r.namespaced(name),
Selector: sel,
Fn: w,
})
}
func (r *stagingRegistrar) On(event platform.LifecycleEvent, name string, fn platform.LifecycleHandler) {
if !r.validateName(name) {
return
}
if fn == nil {
r.bufferErr(ReasonInvalidHookRegister, fmt.Sprintf("on %q: handler is nil", name))
return
}
if !isValidLifecycleEvent(event) {
r.bufferErr(ReasonInvalidHookRegister, fmt.Sprintf("on %q: invalid LifecycleEvent value %d", name, event))
return
}
r.stagedLifecycles = append(r.stagedLifecycles, hook.LifecycleEntry{
Name: r.namespaced(name),
Event: event,
Fn: fn,
})
}
func (r *stagingRegistrar) Restrict(rule *platform.Rule) {
r.actuallyRestricted = true
if rule == nil {
r.bufferErr(ReasonInvalidRule, "Restrict(nil)")
return
}
// Defensive clone: retaining the caller's *Rule directly would let
// the plugin mutate Allow/Deny/Identities (or even the whole rule)
// after Install returns, bypassing the validation we run on the
// stored copy in validateSelf. Take an independent snapshot of
// every slice field so the post-validation rule is frozen.
cp := *rule
cp.Allow = append([]string(nil), rule.Allow...)
cp.Deny = append([]string(nil), rule.Deny...)
cp.Identities = append([]platform.Identity(nil), rule.Identities...)
r.rules = append(r.rules, &cp)
}
// --- helpers ---
func (r *stagingRegistrar) namespaced(name string) string {
return r.pluginName + "." + name
}
func (r *stagingRegistrar) validateName(name string) bool {
if !hookNamePattern.MatchString(name) {
r.bufferErr(ReasonInvalidHookName, fmt.Sprintf("hookName %q must match ^[a-z0-9][a-z0-9-]*$", name))
return false
}
if r.seenHookNames[name] {
r.bufferErr(ReasonDuplicateHookName, fmt.Sprintf("hookName %q registered twice in same plugin", name))
return false
}
r.seenHookNames[name] = true
return true
}
func (r *stagingRegistrar) validateNonNilSelector(name string, sel platform.Selector) bool {
if sel == nil {
r.bufferErr(ReasonInvalidHookRegister, fmt.Sprintf("hook %q: selector is nil", name))
return false
}
return true
}
func (r *stagingRegistrar) bufferErr(reasonCode, message string) {
r.stagingErrs = append(r.stagingErrs, stagingErr{
reasonCode: reasonCode,
message: message,
})
}
// validateSelf runs after Install returns. It checks:
//
// - any buffered staging error -> abort
// - Restricts declared but Install did not call r.Restrict -> abort
// - Restricts NOT declared but Install did call r.Restrict -> abort
//
// Returns the first PluginInstallError encountered (callers can use
// errors.As to inspect it). Nil means staging is clean.
func (r *stagingRegistrar) validateSelf(caps platform.Capabilities) error {
if len(r.stagingErrs) > 0 {
first := r.stagingErrs[0]
return &PluginInstallError{
PluginName: r.pluginName,
ReasonCode: first.reasonCode,
Reason: first.message,
}
}
if caps.Restricts && !r.actuallyRestricted {
return &PluginInstallError{
PluginName: r.pluginName,
ReasonCode: ReasonRestrictsMismatch,
Reason: "Capabilities.Restricts=true but Install did not call r.Restrict",
}
}
if !caps.Restricts && r.actuallyRestricted {
return &PluginInstallError{
PluginName: r.pluginName,
ReasonCode: ReasonRestrictsMismatch,
Reason: "Capabilities.Restricts=false but Install called r.Restrict",
}
}
return nil
}
func isValidWhen(w platform.When) bool {
return w == platform.Before || w == platform.After
}
func isValidLifecycleEvent(e platform.LifecycleEvent) bool {
switch e {
case platform.Startup, platform.Shutdown:
return true
}
return false
}
+154
View File
@@ -0,0 +1,154 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform
import (
"fmt"
"strconv"
"strings"
"github.com/larksuite/cli/internal/build"
)
// currentCLIVersion returns the running binary's version, redirectable
// from tests via SetCurrentCLIVersionForTesting. Production reads from
// internal/build.Version, which is set by -ldflags at release time.
var currentCLIVersion = func() string { return build.Version }
// SetCurrentCLIVersionForTesting overrides the version reported to the
// RequiredCLIVersion check. Returns a restore function tests must defer.
func SetCurrentCLIVersionForTesting(v string) func() {
old := currentCLIVersion
currentCLIVersion = func() string { return v }
return func() { currentCLIVersion = old }
}
// satisfiesRequiredCLIVersion reports whether buildVersion meets the
// constraint declared by Plugin.Capabilities().RequiredCLIVersion.
//
// Supported constraint forms (single comparator, no compound):
//
// "" - no requirement (always satisfied)
// "1.2.3" - exact match (equivalent to "=1.2.3")
// "=1.2.3" - exact match
// ">=1.2" - buildVersion >= 1.2 (missing patch -> 0)
// ">1.2" - strict greater than
// "<=1.2" - less than or equal
// "<1.2" - strict less than
//
// Development builds (buildVersion == "DEV" or "") always satisfy the
// constraint; the check is meaningful only for tagged releases.
//
// Returns false and an error when constraint is malformed -- callers
// should treat parse errors as fail-closed so an authoring mistake in
// the plugin does not silently load against the wrong CLI version.
//
// **Order of checks**: constraint syntax is validated FIRST, before the
// DEV-build short-circuit. A malformed constraint is a plugin authoring
// bug; we surface it even on DEV builds so the typo can be caught
// during plugin development instead of waiting for the first tagged
// release to expose it.
func satisfiesRequiredCLIVersion(buildVersion, constraint string) (bool, error) {
constraint = strings.TrimSpace(constraint)
if constraint == "" {
return true, nil
}
op, rhs := splitConstraint(constraint)
rv, err := parseSemverPrefix(rhs)
if err != nil {
return false, fmt.Errorf("invalid RequiredCLIVersion %q: %w", constraint, err)
}
if buildVersion == "" || buildVersion == "DEV" {
return true, nil
}
bv, err := parseSemverPrefix(buildVersion)
if err != nil {
// Build version is unparseable -- treat as DEV so an exotic
// build tag doesn't lock plugins out.
return true, nil //nolint:nilerr // intentional fail-open for unparseable buildVersion
}
cmp := compareSemver(bv, rv)
switch op {
case "=", "":
return cmp == 0, nil
case ">=":
return cmp >= 0, nil
case ">":
return cmp > 0, nil
case "<=":
return cmp <= 0, nil
case "<":
return cmp < 0, nil
default:
return false, fmt.Errorf("invalid RequiredCLIVersion %q: unknown operator %q", constraint, op)
}
}
// splitConstraint extracts the leading comparator (if any) from a
// constraint string. The operator is one of "", "=", ">=", ">", "<=", "<".
func splitConstraint(s string) (op, rest string) {
switch {
case strings.HasPrefix(s, ">="):
return ">=", strings.TrimSpace(s[2:])
case strings.HasPrefix(s, "<="):
return "<=", strings.TrimSpace(s[2:])
case strings.HasPrefix(s, ">"):
return ">", strings.TrimSpace(s[1:])
case strings.HasPrefix(s, "<"):
return "<", strings.TrimSpace(s[1:])
case strings.HasPrefix(s, "="):
return "=", strings.TrimSpace(s[1:])
default:
return "", s
}
}
// parseSemverPrefix parses MAJOR[.MINOR[.PATCH]] and drops any pre-release /
// build suffix. Missing minor / patch default to 0. Accepts a leading "v".
func parseSemverPrefix(s string) (parts [3]int, err error) {
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
if s == "" {
return parts, fmt.Errorf("empty version")
}
// Trim pre-release/build suffix at first '-' or '+'.
for i, c := range s {
if c == '-' || c == '+' {
s = s[:i]
break
}
}
fields := strings.Split(s, ".")
// Reject `1.2.3.4` and longer instead of silently truncating —
// truncation hides the typo and lets a malformed RequiredCLIVersion
// pass validation while the comparator below operates on the wrong
// components. Build-version parsing has its own fail-open guard
// upstream (see satisfiesRequiredCLIVersion comment about exotic
// build tags), so it stays compatible.
if len(fields) > 3 {
return [3]int{}, fmt.Errorf("version %q has more than three numeric components", s)
}
for i, f := range fields {
n, err := strconv.Atoi(strings.TrimSpace(f))
if err != nil || n < 0 {
return [3]int{}, fmt.Errorf("non-numeric component %q in version %q", f, s)
}
parts[i] = n
}
return parts, nil
}
func compareSemver(a, b [3]int) int {
for i := 0; i < 3; i++ {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
return 0
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package internalplatform
import (
"errors"
"testing"
"github.com/larksuite/cli/extension/platform"
)
func TestSatisfiesRequiredCLIVersion_constraints(t *testing.T) {
cases := []struct {
name string
build string
constraint string
want bool
wantErr bool
}{
{"empty constraint always satisfied", "1.0.0", "", true, false},
{"DEV build always satisfied", "DEV", ">=99.0.0", true, false},
{"empty build counts as DEV", "", ">=99.0.0", true, false},
{"v prefix stripped", "v1.0.28", ">=1.0.0", true, false},
{"exact match implicit operator", "1.0.0", "1.0.0", true, false},
{"exact match explicit =", "1.0.0", "=1.0.0", true, false},
{">= equal", "1.0.0", ">=1.0.0", true, false},
{">= higher", "1.2.0", ">=1.0.0", true, false},
{">= lower fails", "1.0.0", ">=2.0.0", false, false},
{"> strict higher", "1.0.1", ">1.0.0", true, false},
{"> equal fails", "1.0.0", ">1.0.0", false, false},
{"<= equal", "1.0.0", "<=1.0.0", true, false},
{"<= higher fails", "2.0.0", "<=1.0.0", false, false},
{"< strict lower", "0.9.0", "<1.0.0", true, false},
{"missing patch defaults to 0", "1.0", ">=1.0.0", true, false},
{"constraint with pre-release suffix", "1.0.0-rc1", ">=1.0.0", true, false},
{"malformed constraint returns error", "1.0.0", ">=abc", false, true},
{"malformed constraint errors on DEV too", "DEV", ">=abc", false, true},
{"malformed constraint errors on empty build", "", ">=zzz", false, true},
{"unparseable build version treated as DEV", "abc", ">=1.0.0", true, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := satisfiesRequiredCLIVersion(tc.build, tc.constraint)
if tc.wantErr {
if err == nil {
t.Errorf("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.want {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}
// A plugin whose RequiredCLIVersion exceeds the running build must
// abort install with reason_code capability_unmet. The plugin's
// FailurePolicy then decides whether the abort bubbles up.
func TestInstallOne_RequiredCLIVersion_UnmetFailClosedAborts(t *testing.T) {
restore := SetCurrentCLIVersionForTesting("1.0.0")
t.Cleanup(restore)
platform.ResetForTesting()
t.Cleanup(platform.ResetForTesting)
platform.Register(&capVersionPlugin{
name: "needs-future",
requirement: ">=99.0.0",
fail: platform.FailClosed,
})
_, err := InstallAll(platform.RegisteredPlugins(), nil)
if err == nil {
t.Fatal("expected FailClosed install error, got nil")
}
var pi *PluginInstallError
if !errors.As(err, &pi) {
t.Fatalf("expected *PluginInstallError, got %T", err)
}
if pi.ReasonCode != ReasonCapabilityUnmet {
t.Errorf("reason_code = %q, want %q", pi.ReasonCode, ReasonCapabilityUnmet)
}
}
// FailOpen plugin with unmet RequiredCLIVersion is skipped (warning),
// other plugins still install.
func TestInstallOne_RequiredCLIVersion_UnmetFailOpenSkips(t *testing.T) {
restore := SetCurrentCLIVersionForTesting("1.0.0")
t.Cleanup(restore)
platform.ResetForTesting()
t.Cleanup(platform.ResetForTesting)
platform.Register(&capVersionPlugin{
name: "future-failopen",
requirement: ">=99.0.0",
fail: platform.FailOpen,
})
result, err := InstallAll(platform.RegisteredPlugins(), nil)
if err != nil {
t.Fatalf("FailOpen unmet must not bubble up, got: %v", err)
}
if result.Registry == nil {
t.Errorf("Registry should be non-nil even after FailOpen skip")
}
}
// A plugin authoring error in RequiredCLIVersion (parse failure) must
// abort installation UNCONDITIONALLY. Even FailOpen cannot mask a
// typo in the constraint string -- the plugin author asked the host
// to do something it cannot parse, and silently skipping would hide
// the bug from CI.
//
// Implementation: parse errors return ReasonInvalidCapability, which
// isUntrustedConfigError lists alongside restricts_mismatch so
// InstallAll's switch treats it as a hard abort.
func TestInstallOne_RequiredCLIVersion_MalformedAbortsRegardlessOfFailurePolicy(t *testing.T) {
restore := SetCurrentCLIVersionForTesting("1.0.0")
t.Cleanup(restore)
platform.ResetForTesting()
t.Cleanup(platform.ResetForTesting)
// FailOpen + malformed constraint: still aborts.
platform.Register(&capVersionPlugin{
name: "typo",
requirement: ">=abc",
fail: platform.FailOpen,
})
_, err := InstallAll(platform.RegisteredPlugins(), nil)
if err == nil {
t.Fatal("expected malformed constraint to abort even FailOpen, got nil")
}
var pi *PluginInstallError
if !errors.As(err, &pi) {
t.Fatalf("expected *PluginInstallError, got %T", err)
}
if pi.ReasonCode != ReasonInvalidCapability {
t.Errorf("reason_code = %q, want %q", pi.ReasonCode, ReasonInvalidCapability)
}
}
// A plugin whose RequiredCLIVersion is satisfied installs normally.
func TestInstallOne_RequiredCLIVersion_SatisfiedInstalls(t *testing.T) {
restore := SetCurrentCLIVersionForTesting("1.5.0")
t.Cleanup(restore)
platform.ResetForTesting()
t.Cleanup(platform.ResetForTesting)
platform.Register(&capVersionPlugin{
name: "ok",
requirement: ">=1.0.0",
fail: platform.FailClosed,
})
if _, err := InstallAll(platform.RegisteredPlugins(), nil); err != nil {
t.Errorf("expected install success, got %v", err)
}
}
type capVersionPlugin struct {
name string
requirement string
fail platform.FailurePolicy
}
func (p *capVersionPlugin) Name() string { return p.name }
func (p *capVersionPlugin) Version() string { return "0.0.1" }
func (p *capVersionPlugin) Capabilities() platform.Capabilities {
return platform.Capabilities{
RequiredCLIVersion: p.requirement,
FailurePolicy: p.fail,
}
}
func (p *capVersionPlugin) Install(platform.Registrar) error { return nil }