mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 18:13:01 +08:00
Add a single public extension contract under extension/platform: integrators
implement the Plugin interface and register Observers, Wrappers, Lifecycle
handlers, and pruning Rules through the Registrar in one Install call.
Command pruning:
- Rule (Allow / Deny / MaxRisk / Identities) with doublestar globs
- 4-axis AND evaluation, parent-group aggregation, unknown-risk allow
- Sources: Plugin.Restrict (single-rule) and ~/.lark-cli/policy.yml
- Plugin path is fail-closed (envelope on rule error / multiple Restrict);
yaml path is fail-open (warning, CLI continues)
- strict-mode stubs now also write the denial annotation so the hook
layer's denial guard physically isolates Wrap chains on them
- HOME path never leaked through policy_source label
Hook framework:
- Observer (panic-safe, Before/After), Wrapper (middleware, may short-circuit
via AbortError), Lifecycle (Startup + Shutdown only)
- Recover guards every plugin entry point: Capabilities(), Install(),
Wrapper factory composition AND inner Handler, Lifecycle handlers
- namespacedWrap copies AbortError so a plugin's package-level sentinel
is never mutated across concurrent invocations
- Selector unknown-risk uniform: ByExactRisk / ByWrite / ByReadOnly never
match unannotated commands; safety-side hooks opt in via
ByWrite().Or(ByUnknownRisk())
Bootstrap orchestration (cmd/build.go + cmd/policy.go):
- InstallAll uses a staging Registrar + atomic commit
- FailClosed plugin install / Plugin.Restrict conflict / Startup handler
failure each install a structured envelope guard at every dispatch path
- walkGuard neutralises every cobra bypass we know of (PersistentPreRunE
first-wins, ValidateArgs, ParseFlags, legacyArgs, __complete /
__completeNoDesc, non-runnable groups, required-arg subcommands)
- cmd/root.go::Execute calls hook.Emit(Shutdown, runErr) after
rootCmd.Execute; isCompletionCommand skips both __complete and
__completeNoDesc so Tab completion never triggers Shutdown handlers
Capabilities consistency:
- Restricts=true must declare FailurePolicy=FailClosed
- RequiredCLIVersion (semver constraint) is validated against build.Version;
a malformed constraint is treated as untrusted-config and aborts
unconditionally, regardless of FailurePolicy (DEV builds included)
JSON envelope contract:
- error.type closed enum: pruning / strict_mode / hook / plugin_install /
plugin_conflict / plugin_lifecycle
- reason_code closed enums per type, all referenced by structured tests
Bootstrap surfaces (new user commands):
- lark-cli config policy show -- JSON view of the active Rule + source
- lark-cli config policy validate -- parse + schema + glob check, no apply
Coverage:
- extension/platform: every public type has a unit test
- internal/{pruning,hook,platformhost,policydecision,cmdmeta}: full coverage
of denial guard isolation, AbortError sentinel safety, observer panic
safety, lifecycle error/panic typing, staging atomic rollback
- cmd/plugin_integration_test.go: end-to-end through buildInternal with
synthetic and real command trees
- cmd/install_guard_test.go: walkGuard covers auth / config / __complete /
__completeNoDesc / non-runnable parents
58 lines
2.1 KiB
Go
58 lines
2.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package platformhost
|
|
|
|
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"
|
|
ReasonDoubleRestrict = "double_restrict"
|
|
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"
|
|
)
|