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

  1. Remove the FailOpen() call — restriction policies require FailClosed
  2. Move FailOpen() before Restrict() only if you also remove the restriction; otherwise it is still invalid
  3. 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

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


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/a0077bac1133969a. Report an issue: GitHub.