larksuite/cli · error
Restrict() requires FailClosed; do not call FailOpen() after
Error message
Restrict() requires FailClosed; do not call FailOpen() after Restrict()
What it means
Build() rejects the plugin when Restrict() rules exist but the final failure policy is FailOpen. Restrict sets FailurePolicy to FailClosed at call time, so this error means FailOpen() was called after Restrict(). FailOpen/FailClosed may be called in either order, so the check runs against the final builder state in Build().
Source
Thrown at extension/platform/builder.go:199
// are copied; Overlay/Base are fs.FS handles retained by reference (an fs.FS is
// a read-only view, not caller-mutable state).
func cloneSkillsOverlay(spec *SkillsOverlay) *SkillsOverlay {
cp := *spec
cp.Allow = append([]string(nil), spec.Allow...)
cp.Remove = append([]string(nil), spec.Remove...)
cp.ReferenceRemaps = append([]SkillRefRemap(nil), spec.ReferenceRemaps...)
return &cp
}
// 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 (b *Builder) Build() (Plugin, error) {
if len(b.rules) > 0 && b.caps.FailurePolicy == FailOpen {
b.errs = append(b.errs, errors.New(
"Restrict() requires FailClosed; do not call FailOpen() after Restrict()"))
}
if b.skillsOverlay != nil && b.caps.FailurePolicy == FailOpen {
b.errs = append(b.errs, errors.New(
"EmbeddedSkills() requires FailClosed; do not call FailOpen() after EmbeddedSkills()"))
}
if len(b.errs) > 0 {
return nil, errors.Join(b.errs...)
}
return &builtPlugin{
name: b.name,
version: b.version,
caps: b.caps,
actions: b.actions,
rules: b.rules,
skillsOverlay: b.skillsOverlay,
}, nil
}View on GitHub (pinned to 7fd6ef3c07)
Solutions
- Remove the FailOpen() call — restriction policies require FailClosed
- Move FailOpen() before Restrict() only if you also remove the restriction; otherwise it is still invalid
- If fail-open behavior is genuinely needed, do not use Restrict; define the plugin without restrict rules
Example fix
// before b.Restrict(rule).FailOpen() // invalid // after b.Restrict(rule) // FailurePolicy stays FailClosed
Defensive patterns
Strategy: validation
Validate before calling
if len(rules) > 0 && policy == FailOpen {
return nil, fmt.Errorf("restrict rules require FailClosed")
} Try / catch
p, err := builder.Build()
if err != nil {
// errors.Join of all builder errors; check for Restrict/FailClosed conflict
return nil, fmt.Errorf("plugin build failed: %w", err)
} Prevention
- Never chain FailOpen() after Restrict(); the last policy setter wins, so keep FailClosed last when restricting
- Do not add FailOpen in shared builder helpers for plugins that use restrictions
- Rely on Build() (not MustBuild) during development to see all accumulated errors
When it happens
Trigger: b.Restrict(rule) followed by b.FailOpen(), then Build(). The check is len(b.rules) > 0 && FailurePolicy == FailOpen at Build time.
Common situations: Fluent-chain refactors that append FailOpen for 'resilience'; shared builder helpers that set FailOpen unconditionally; misunderstanding that setter order matters (it does — the final state is what counts).
Related errors
- EmbeddedSkills() requires FailClosed; do not call FailOpen()
- Restrict(nil): rule must not be nil
- EmbeddedSkills(nil): spec must not be nil
- EmbeddedSkills() called more than once; a plugin owns at mos
- Invalid column: {column!r}
AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04).
Data as JSON: /api/errors/a0077bac1133969a.
Report an issue: GitHub.