Files
larksuite-cli/internal/platform/error.go
sang-neo03 50b3f0a2af feat(platform): support multiple policy rules per plugin (#1182)
* feat(platform): support multiple policy rules per plugin

Extend the command policy framework from single-Rule to multi-Rule
semantics. A plugin (or policy.yml) may now contribute several scoped
Rules; the engine combines them with OR -- a command is allowed when it
satisfies every axis of at least one rule. This lets one integration
apply different risk ceilings and identity restrictions to different
command groups.

The cross-plugin fail-closed boundary is preserved: two distinct plugins
both calling Restrict still aborts startup (multiple_restrict_plugins).
Single-Rule behaviour is fully backward compatible -- the rejection
reason_code / rule_name / envelope shape are byte-for-byte unchanged;
multi-rule rejection surfaces the aggregate reason_code no_matching_rule.

- engine: New keeps single-rule compat, add NewSet for OR over rules
- resolver: dedupe by owner (one plugin may contribute many rules),
  return []*Rule; yaml gains a top-level rules: list
- registrar/builder/staging: Restrict may be called more than once;
  retire the double_restrict error
- config policy show / config plugins show: emit a rules array
- inventory: PluginEntry.Rules is now a slice (fixes last-rule-wins
  overwrite when a plugin contributes multiple rules)

* fix(platform): clone rules in Builder.Restrict and inventory snapshot

Address review feedback. Builder.Restrict stored the caller's *Rule
directly, so reusing and mutating one Rule object across multiple
Restrict calls collapsed entries to the last mutation; clone the rule and
its slices on append, mirroring the staging registrar.

BuildInventory likewise reused the source Allow/Deny/Identities slices;
copy them when building the RuleView snapshot instead of relying on
cloneInventory downstream.

Add a regression test: reusing and mutating one Rule across two Restrict
calls now yields two independent rules.

* fix(platform): skip yaml when a plugin owns policy; reject empty rules list

Two policy-config robustness fixes from review:

- A malformed ~/.lark-cli/policy.yml could abort a plugin-governed
  binary. applyUserPolicyPruning read yaml before resolving, and
  build.go fail-closes on any policy error when a plugin is present.
  Plugin rules shadow yaml anyway, so skip reading yaml entirely when a
  plugin contributed rules -- an unrelated broken file on the user's
  machine can no longer lock the CLI.

- A present-but-empty "rules: []" collapsed to a single all-zero Rule
  that allows every annotated command ("looks like policy, enforces
  almost nothing"). yaml.Parse now distinguishes absent from
  present-but-empty (Rules is a pointer) and rejects the empty list.

Add regression tests for both.
2026-05-30 17:05:33 +08:00

57 lines
2.0 KiB
Go

// 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"
)