Skip to content

feat(extension): present restricted commands as absent and trim skills - #1837

Open
evandance wants to merge 1 commit into
mainfrom
feat/plugin-trim
Open

feat(extension): present restricted commands as absent and trim skills#1837
evandance wants to merge 1 commit into
mainfrom
feat/plugin-trim

Conversation

@evandance

@evandance evandance commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Let integrators ship a reduced lark-cli distribution without exposing plugin-policy details or leaving unusable command, recovery, and skill affordances behind. Existing Restrict behavior remains compatible; absence-style presentation is an explicit wrapper-host choice through ExecuteWithOptions.

Changes

  • Preserve Execute() int and add ExecuteWithOptions(opts ...BuildOption) int with ConcealRestrictedCommands, UnavailableMessage, and HidePolicyDiagnostics.
  • Keep enforcement and presentation separate. Plugin-denied commands in an opted-in distribution appear absent and return validation/command_unavailable; legacy Restrict and YAML-policy denials retain their existing envelopes.
  • Build an immutable, exact-path surface plan per Cobra tree. Help, completion, root groups, global flags, notices, diagnostics, and recovery guidance project from that build-local state.
  • Preserve exact denied-command dispatch as an unavailable stub. Plugin observers still see the attempted invocation; business wrappers are not entered.
  • Project the root quickstart, examples, and usage synopsis from target-aware sections. A reduced build removes concealed api, schema, calendar, and mail examples without leaving an empty examples heading; every declared target is contract-tested against the real default command tree.
  • Reject help <child> whenever any ancestor is concealed, closing the Cobra help traversal that could reveal descendants of an unavailable group.
  • Complete user/bot permission recovery once at the root error boundary from typed identity, missing_scopes, subtype, and console_url facts. Framework-owned auth outputs and typed-error recovery no longer need distribution knowledge or handwritten auth login hints; ordinary business-domain Tips remain outside this migration boundary.
  • Preserve producer errors by cloning before presentation. Target-only recovery has a target-free fallback, later hint/message enrichment survives projection, and validation messages can remove only the unavailable command fragment while retaining another valid recovery path.
  • Add an AST lint rule that rejects new typed-error message/hint literals containing opaque framework command recovery (auth login, config bind, profile add) outside the structured recovery model.
  • Add fail-closed skill composition through SkillsOverlay{Base, Allow, Remove, Overlay, ReferenceRemaps}. Missing wrapper base content, invalid trees, conflicting owners, dangling remaps, install failures, and plugin-FS panics become controlled build/runtime errors instead of silently restoring host defaults or crashing.
  • Add structured SkillRef resolution and platform.RemapSkillRef. Whole-skill remaps preserve relative paths, exact remaps take precedence, and unresolved references remove their complete help fragment.
  • Move docs skill guidance out of docs-specific Go presentation code into affordance/docs.md. Domain and shortcut help now use the same generic affordance + skill resolver path, including replacement/removal/concealment.
  • Document the two independent wrapper assets: cmd.SetEmbeddedSkillContent owns the runtime skill tree and cmd.SetEmbeddedAffordanceContent owns command guidance. The external-module E2E builds wrappers exactly this way.
  • Keep BuildOptions single-evaluation, root-group finalization build-local, Cobra help registration framework-owned, and the new presentation pass decomposed into typed projection stages.

Compatibility

  • Default command execution, data output, error category/subtype/param/machine fields, exit status, completion, skills commands, profile parsing, legacy Restrict behavior, and YAML policy behavior remain unchanged.
  • User authorization and missing-scope hint copy is intentionally standardized at the root presenter. User recovery points to auth login only when that command is referenceable; bot recovery points to app/developer configuration; reduced builds receive a command-independent authorization fallback.
  • The intentional default-build presentation change remains limited to docs help: the old hand-authored MUST prose and flag-description skill pointers are replaced by generic risk, affordance, and resolvable skills read sections. The docs commands and exit codes are unchanged.
  • Execute() int, existing Capabilities/Rule usage, and the public two-argument help preparation functions retain source compatibility.
  • Command restriction and skill selection remain independent axes. A wrapper that replaces/removes skills controls which skill references survive; it does not implicitly remove business commands.

Test Plan

  • make unit-test (race-enabled supported packages)
  • go test ./tests/plugin_e2e -count=1 (real external wrapper modules)
  • go vet ./...
  • gofmt -l . produces no output
  • go mod tidy leaves go.mod and go.sum unchanged
  • golangci-lint v2.1.6 reports 0 issues
  • go test -C lint ./errscontract
  • python3 skills/lark-slides/scripts/xml_text_overlap_lint_test.py (124 tests from rebased main)
  • Repository lint scan accepts the structured-recovery gate (existing typed-resolution fallback warning remains advisory)
  • Latest origin/main: 0 behind / 1 ahead
  • origin/main/final-head black-box A/B: representative default help paths are byte-identical, including config init --help; only the declared docs-help presentation differs
  • Permission-grant probes preserve the default auth login recovery and project it to a target-free fallback only when that command is concealed
  • Configured probes pin the intentional authorization-hint standardization while preserving drive +search --mine's existing default wire
  • GitHub CI on the updated head

External fork E2E covers wrapper asset wiring, Allow/Remove, complete Base replacement, whole-skill and exact-reference remapping, docs pointer rendering/removal, root-help and generated-method schema projection, concealed-parent help rejection, framework-owned and typed-error target-free authorization recovery, retained profile remove/config init recovery after concealing profile add/config bind, concealed skills commands, legacy Restrict reason compatibility, fail-closed installation, and plugin-FS runtime panic containment.

Related Issues

  • None

@evandance
evandance requested a review from liangshuo-1 as a code owner July 9, 2026 09:13
@evandance evandance added the enhancement New feature or request label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds embedded skill overlays for callers and plugins, validates and composes skill filesystems, introduces plugin-specific unavailable errors, and applies plugin-denied policy state to CLI help, flags, notices, diagnostics, and recovery hints.

Changes

Embedded skills and plugin integration

Layer / File(s) Summary
Skills overlay contracts and resolver
extension/platform/*, internal/skillpolicy/*
Adds overlay APIs, builder and registrar support, validation, single-owner enforcement, and merged filesystem behavior.
Plugin installation and inventory
internal/platform/*, cmd/config/plugins.go, cmd/config/plugins_test.go
Stages overlays, validates capabilities, records embedded skills and diagnostic visibility in inventory, and exposes them in plugin configuration output.
CLI build integration
cmd/build.go, cmd/platform_guards.go, cmd/skill_customization_test.go
Adds WithEmbeddedSkills, resolves caller and plugin overlays during assembly, resets build snapshots, and guards resolution failures.

Policy-aware command presentation

Layer / File(s) Summary
Policy denial engine and bootstrap state
errs/subtypes.go, internal/cmdpolicy/*, cmd/platform_bootstrap.go
Adds command-unavailable errors, plugin denial messages, active denial snapshots, diagnostic path enumeration, and plugin-domain tracking.
Hints, flags, help, and diagnostics
internal/policystate/*, internal/auth/*, internal/client/*, internal/core/*, internal/errclass/*, cmd/flag_gate.go, cmd/presentation.go, cmd/root.go, cmd/prune.go, internal/hook/install.go
Gates recovery hints, hides and rejects denied global flags, intercepts unavailable help, suppresses notices, and conceals or retires diagnostics according to plugin policy.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: size/L, domain/base, feature

Suggested reviewers: liangshuo-1, Roy-oss1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and matches the main change: restricted commands are presented as absent and embedded skills are trimmed.
Description check ✅ Passed The description matches the required template with Summary, Changes, Test Plan, and Related Issues sections filled in.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plugin-trim

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/XL Architecture-level or global-impact change label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@4d15abb5473db450b77ce2f924b22d94f202642b

🧩 Skill update

npx skills add larksuite/cli#feat/plugin-trim -y -g

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.58657% with 318 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.33%. Comparing base (1f565a2) to head (4d15abb).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cmd/root.go 75.88% 20 Missing and 14 partials ⚠️
internal/skillpolicy/pluginfs.go 75.19% 18 Missing and 14 partials ⚠️
internal/recovery/render.go 68.36% 17 Missing and 14 partials ⚠️
internal/recovery/hint.go 64.55% 25 Missing and 3 partials ⚠️
internal/skillpolicy/overlay.go 79.24% 16 Missing and 6 partials ⚠️
cmd/presentation.go 87.50% 13 Missing and 8 partials ⚠️
internal/errclass/classify.go 74.62% 16 Missing and 1 partial ⚠️
internal/identitydiag/diagnostics.go 53.33% 14 Missing ⚠️
internal/skillpolicy/resolver.go 88.39% 7 Missing and 6 partials ⚠️
internal/skillref/resolver.go 84.37% 5 Missing and 5 partials ⚠️
... and 27 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1837      +/-   ##
==========================================
+ Coverage   75.19%   75.33%   +0.14%     
==========================================
  Files         914      928      +14     
  Lines       96789    98139    +1350     
==========================================
+ Hits        72778    73934    +1156     
- Misses      18413    18534     +121     
- Partials     5598     5671      +73     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (7)
extension/platform/registrar.go (1)

39-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc comment should start with EmbeddedSkills, not Skills.

Go doc convention expects the first word to match the symbol name so go doc produces a correct synopsis. Currently it reads "Skills contributes a SkillsOverlay..." instead of "EmbeddedSkills contributes a SkillsOverlay...".

📝 Proposed fix
-	// Skills contributes a SkillsOverlay customizing the CLI's embedded skill
+	// EmbeddedSkills contributes a SkillsOverlay customizing the CLI's embedded skill
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/platform/registrar.go` around lines 39 - 51, Update the doc comment
on EmbeddedSkills in registrar.go so the first word matches the method name;
change the opening sentence from “Skills contributes...” to “EmbeddedSkills
contributes...” while keeping the rest of the comment unchanged. This ensures go
doc generates the correct synopsis for the EmbeddedSkills symbol.
internal/skillpolicy/resolver_test.go (1)

107-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for Remove+Overlay targeting the same skill.

The composition order (Base → Allow → Remove → Overlay) means an Overlay entry should win over a Remove entry for the same skill name. effectiveRemove documents this ("Overlay entries are never folded in — the overlay FS resolves them from the upper layer before the whiteout applies"), but no test explicitly verifies this interaction. A same-name Remove+Overlay test would lock in this precedence behavior.

📝 Suggested test
// Overlay wins over Remove for the same skill name: the overlay
// entry is served from the upper layer before the whiteout applies.
func TestResolve_OverlayWinsOverRemove(t *testing.T) {
	overlay := skillFS(map[string]string{"lark-a/SKILL.md": "overlaid a"})
	got := mustResolve(t, baseTree(), &platform.SkillsOverlay{
		Remove:  []string{"lark-a"},
		Overlay: overlay,
	})
	if want := []string{"lark-a", "lark-b", "lark-shared"}; !equalStrings(topLevel(t, got), want) {
		t.Errorf("top level = %v, want %v (overlay restores removed skill)", topLevel(t, got), want)
	}
	if c := readFile(t, got, "lark-a/SKILL.md"); c != "overlaid a" {
		t.Errorf("lark-a content = %q, want overlaid a (overlay wins over remove)", c)
	}
}

Also applies to: 168-176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skillpolicy/resolver_test.go` around lines 107 - 119, Add a test in
resolver_test.go that covers Remove and Overlay targeting the same skill name,
since the current tests only verify same-name Overlay replacing the base skill.
Use mustResolve with platform.SkillsOverlay on baseTree, setting both Remove and
Overlay for lark-a, and assert that Overlay wins by restoring lark-a in topLevel
and serving lark-a/SKILL.md from the overlay. This should lock in the precedence
behavior documented by effectiveRemove and the Base → Allow → Remove → Overlay
composition.
extension/platform/README.md (1)

117-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify Overlay vs Base distinction in the safety contract.

Line 121 reads "Overlay adds/replaces ones, or swap the whole Base" — the verb "swap" breaks the parallel structure ("keeps", "drops", "adds/replaces") and conflates the Overlay field with the Base field. Consider separating them for clarity.

📝 Suggested rewording
-	`Remove` wins over `Allow`, and `Overlay` entries are exempt), `Remove`
-	drops skills, `Overlay` adds/replaces ones, or swap the whole `Base` —
-	layered over the CLI default.
+	`Remove` wins over `Allow`, and `Overlay` entries are exempt), `Remove`
+	drops skills, `Overlay` adds/replaces ones, and `Base` swaps the
+	entire base tree — all layered over the CLI default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/platform/README.md` around lines 117 - 145, Clarify the
safety-contract wording in the EmbeddedSkills docs: the current sentence in the
README blurs `Overlay` and `Base` by using “swap,” which breaks the parallel
list of actions. Update the text around `EmbeddedSkills()`, `Allow`, `Remove`,
`Overlay`, and `Base` so `Overlay` is described as adding/replacing entries and
`Base` is described separately as replacing the whole tree, keeping the phrasing
parallel and unambiguous.
internal/skillpolicy/overlay.go (1)

37-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the fs.ReadDir(upper, ".") error is intentionally ignored.

If the upper FS root is unreadable, upperNames stays empty and route() silently falls through to lower for all paths, while mergedRoot() (line 140) will still attempt the same read and propagate the error. The resolver likely validates the upper FS before calling newOverlayFS, but a one-line comment would prevent future readers from mistaking this for a bug.

📝 Suggested comment
 	if upper != nil {
-		if entries, err := fs.ReadDir(upper, "."); err == nil {
+		// upperNames is populated eagerly so route() can decide
+		// without per-call ReadDir. A read failure here is benign:
+		// the resolver validates the overlay FS before calling
+		// newOverlayFS, and mergedRoot() will surface the error
+		// if it ever occurs at read time.
+		if entries, err := fs.ReadDir(upper, "."); err == nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skillpolicy/overlay.go` around lines 37 - 57, Add a brief comment in
newOverlayFS explaining that the fs.ReadDir(upper, ".") error is intentionally
ignored because an unreadable upper root just leaves upperNames empty and lets
route() fall back to lower, while mergedRoot() handles and surfaces the error
separately. Keep the note near the upper != nil / fs.ReadDir block so future
readers understand the intended behavior.
cmd/presentation.go (1)

104-124: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

sample denial can be picked from the wrong plugin when multiple plugins restrict the same domain group.

sample is chosen by matching only path-prefix + IsPluginPolicySource, without pinning to a single owning PolicySource. If two different plugins each deny distinct sub-paths under the same top-level group (e.g. one restricts config/policy, another config/plugins), the aggregate denial installed on the group inherits RuleName/DeniedMessage from whichever entry the (randomized) map iteration happens to hit first, giving inconsistent attribution across runs.

Given this is a narrower edge case (only surfaces with multiple plugins denying disjoint paths under one diagnostic group), please confirm whether that scenario is reachable in practice; if so, consider deterministically selecting sample (e.g. sort candidates or require a single denying source per group) rather than relying on map iteration order.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/presentation.go` around lines 104 - 124, The aggregate group denial in
presentation logic is attributing metadata from an arbitrary plugin because
`sample` is taken from the first matching entry in `denied`, which depends on
map iteration order. Update the selection in `cmd/presentation.go` around the
`sample`/`Apply` block to deterministically choose a single owning
`PolicySource` for the group (or otherwise enforce one source per group), and
only then copy `RuleName`, `DeniedMessage`, and related fields into the
synthesized denial.
internal/cmdpolicy/apply.go (1)

133-249: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

BuildUnavailableError duplicates messageOf's resolution logic.

messageOf (lines 144-149) and BuildUnavailableError (lines 156-159) implement the identical "DeniedMessage or DefaultUnavailableMessage" fallback independently. installDenyStub even calls messageOf(d) for the annotation just above where BuildUnavailableError re-derives the same value. Consolidating avoids future divergence if the fallback logic changes.

♻️ Proposed fix
 func BuildUnavailableError(path string, d Denial) *errs.ValidationError {
-	msg := d.DeniedMessage
-	if msg == "" {
-		msg = DefaultUnavailableMessage
-	}
-	return errs.NewValidationError(errs.SubtypeCommandUnavailable, "%s", msg).
+	return errs.NewValidationError(errs.SubtypeCommandUnavailable, "%s", messageOf(d)).
 		WithCause(CommandDeniedFromDenial(path, d))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cmdpolicy/apply.go` around lines 133 - 249, BuildUnavailableError
duplicates the denied-message fallback already handled by messageOf, so the two
paths can drift. Update BuildUnavailableError to reuse messageOf(d) instead of
re-deriving the default message, keeping the existing behavior for plugin
sources and the Denial capture in installDenyStub consistent. This change should
be made in the BuildUnavailableError and messageOf helpers so there is a single
source of truth for the unavailable-message resolution.
internal/cmdpolicy/diagnostic.go (1)

35-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sort DiagnosticPaths() output for deterministic ordering.

Map iteration in Go is non-deterministic, so the returned slice order varies between calls. While the current downstream consumer (cmd/presentation.go) only iterates to hide paths—making order irrelevant for correctness—any future test that asserts the slice directly will be flaky. Sorting is a one-liner that makes the exported API contract deterministic.

♻️ Proposed fix: sort output
 func DiagnosticPaths() []string {
 	out := make([]string, 0, len(diagnosticPaths))
 	for p := range diagnosticPaths {
 		out = append(out, p)
 	}
+	sort.Strings(out)
 	return out
 }

You'll also need to add "sort" to the import block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cmdpolicy/diagnostic.go` around lines 35 - 41, DiagnosticPaths()
currently returns map keys in non-deterministic order because it iterates
diagnosticPaths directly, so make the exported result deterministic by sorting
the slice before returning it. Update DiagnosticPaths() in diagnostic.go to
collect the keys as it does now, then apply a sort step using the standard sort
package, and add sort to the imports so any callers and tests get stable
ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/config/show.go`:
- Around line 59-64: Add test coverage for the profile hint gate in
configShowRun: the existing no-active-profile case in config_test.go should be
expanded to verify both outcomes of policystate.DomainDeniedByPlugin("profile").
Add one test that stubs the domain as allowed and asserts the returned
ConfigError from configShowRun includes the “run: lark-cli profile list” hint,
and another that stubs it as denied and asserts the hint is omitted. Use the no
active profile path and configShowRun as the main symbols to locate the logic.

In `@cmd/presentation.go`:
- Around line 52-72: The message selection in retireDiagnostics is picking any
plugin-sourced denial instead of the one owned by
hideDiagnosticsOwner(installResult.Plugins), which can make the diagnostics
message nondeterministic. Update the loop in retireDiagnostics to match on the
computed source value as well as cmdpolicy.IsPluginPolicySource, so only the
denial from the actual HideDiagnostics owner can populate message before
cmdpolicy.Apply builds the diagnostics map.

In `@cmd/prune.go`:
- Around line 109-116: Add tests for the config-denied hint branch in
strictModeStubFrom, since cmd/prune_test.go currently only covers the default
stub envelope. Extend the existing prune tests to exercise both outcomes of
policystate.DomainDeniedByPlugin("config"): one case where it returns true and
the returned validation error hint omits stubHint, and one case where it returns
false and the hint includes it. Use strictModeStubFrom, DomainDeniedByPlugin,
and the validation error/hint assertions to keep the conditional behavior locked
in.

In `@cmd/skill_customization_test.go`:
- Around line 144-167: In TestBuildInternal_invalidSkillsOverlayGuard, replace
the brittle Hint substring check on errs.ValidationError with typed metadata
assertions using errs.ProblemOf, mirroring
TestBuildInternal_multipleSkillPluginsGuard. Verify the returned error preserves
the expected category/subtype/param for invalid_skills_overlay and keep the
existing errors.As check so the test validates structured error data instead of
message text alone.
- Around line 198-221: The conflict test for buildInternal only checks
verr.Hint, so update TestBuildInternal_withSkillsPlusPluginConflicts to assert
typed error metadata as well. Use errs.ProblemOf on the returned
*errs.ValidationError from leaf.RunE and verify the expected
category/subtype/param (matching the pattern already used in
TestBuildInternal_multipleSkillPluginsGuard), instead of relying on a Hint
substring alone.

In `@internal/platform/inventory_test.go`:
- Line 39: Add a focused BuildInventory test that passes a non-nil
SkillsInventorySource instead of nil so the EmbeddedSkills path is exercised.
Update the existing internalplatform.BuildInventory call in inventory_test.go
and assert the resulting inventory includes the embedded skills behavior, using
BuildInventory, EmbeddedSkills, and SkillsInventorySource as the key symbols to
locate the change.

In `@internal/platform/inventory.go`:
- Around line 188-194: In BuildInventory, the EmbeddedSkills view is only
cloning View.Remove while leaving View.Allow shared, which is inconsistent with
cloneInventory and stagingRegistrar.EmbeddedSkills. Update the code that builds
v from sk.View to defensively clone both Allow and Remove before assigning
entry.EmbeddedSkills, using the same pattern already used elsewhere so later
mutations to the source skill view cannot affect the inventory data.

---

Nitpick comments:
In `@cmd/presentation.go`:
- Around line 104-124: The aggregate group denial in presentation logic is
attributing metadata from an arbitrary plugin because `sample` is taken from the
first matching entry in `denied`, which depends on map iteration order. Update
the selection in `cmd/presentation.go` around the `sample`/`Apply` block to
deterministically choose a single owning `PolicySource` for the group (or
otherwise enforce one source per group), and only then copy `RuleName`,
`DeniedMessage`, and related fields into the synthesized denial.

In `@extension/platform/README.md`:
- Around line 117-145: Clarify the safety-contract wording in the EmbeddedSkills
docs: the current sentence in the README blurs `Overlay` and `Base` by using
“swap,” which breaks the parallel list of actions. Update the text around
`EmbeddedSkills()`, `Allow`, `Remove`, `Overlay`, and `Base` so `Overlay` is
described as adding/replacing entries and `Base` is described separately as
replacing the whole tree, keeping the phrasing parallel and unambiguous.

In `@extension/platform/registrar.go`:
- Around line 39-51: Update the doc comment on EmbeddedSkills in registrar.go so
the first word matches the method name; change the opening sentence from “Skills
contributes...” to “EmbeddedSkills contributes...” while keeping the rest of the
comment unchanged. This ensures go doc generates the correct synopsis for the
EmbeddedSkills symbol.

In `@internal/cmdpolicy/apply.go`:
- Around line 133-249: BuildUnavailableError duplicates the denied-message
fallback already handled by messageOf, so the two paths can drift. Update
BuildUnavailableError to reuse messageOf(d) instead of re-deriving the default
message, keeping the existing behavior for plugin sources and the Denial capture
in installDenyStub consistent. This change should be made in the
BuildUnavailableError and messageOf helpers so there is a single source of truth
for the unavailable-message resolution.

In `@internal/cmdpolicy/diagnostic.go`:
- Around line 35-41: DiagnosticPaths() currently returns map keys in
non-deterministic order because it iterates diagnosticPaths directly, so make
the exported result deterministic by sorting the slice before returning it.
Update DiagnosticPaths() in diagnostic.go to collect the keys as it does now,
then apply a sort step using the standard sort package, and add sort to the
imports so any callers and tests get stable ordering.

In `@internal/skillpolicy/overlay.go`:
- Around line 37-57: Add a brief comment in newOverlayFS explaining that the
fs.ReadDir(upper, ".") error is intentionally ignored because an unreadable
upper root just leaves upperNames empty and lets route() fall back to lower,
while mergedRoot() handles and surfaces the error separately. Keep the note near
the upper != nil / fs.ReadDir block so future readers understand the intended
behavior.

In `@internal/skillpolicy/resolver_test.go`:
- Around line 107-119: Add a test in resolver_test.go that covers Remove and
Overlay targeting the same skill name, since the current tests only verify
same-name Overlay replacing the base skill. Use mustResolve with
platform.SkillsOverlay on baseTree, setting both Remove and Overlay for lark-a,
and assert that Overlay wins by restoring lark-a in topLevel and serving
lark-a/SKILL.md from the overlay. This should lock in the precedence behavior
documented by effectiveRemove and the Base → Allow → Remove → Overlay
composition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26da75ec-40e3-4e44-b400-0933e3775fb9

📥 Commits

Reviewing files that changed from the base of the PR and between d87d9b4 and 9cba7bd.

📒 Files selected for processing (44)
  • cmd/build.go
  • cmd/config/plugins.go
  • cmd/config/plugins_test.go
  • cmd/config/show.go
  • cmd/flag_gate.go
  • cmd/platform_bootstrap.go
  • cmd/platform_bootstrap_test.go
  • cmd/platform_guards.go
  • cmd/presentation.go
  • cmd/presentation_test.go
  • cmd/prune.go
  • cmd/root.go
  • cmd/root_risk_help_test.go
  • cmd/service/affordance_test.go
  • cmd/skill_customization_test.go
  • errs/subtypes.go
  • extension/platform/README.md
  • extension/platform/builder.go
  • extension/platform/builder_test.go
  • extension/platform/capabilities.go
  • extension/platform/registrar.go
  • extension/platform/rule.go
  • extension/platform/skillsoverlay.go
  • internal/auth/errors.go
  • internal/auth/hint_gate_test.go
  • internal/client/client.go
  • internal/client/hint_gate_test.go
  • internal/cmdpolicy/active.go
  • internal/cmdpolicy/aggregation_test.go
  • internal/cmdpolicy/apply.go
  • internal/cmdpolicy/denial.go
  • internal/cmdpolicy/diagnostic.go
  • internal/cmdpolicy/engine.go
  • internal/cmdpolicy/source_label_test.go
  • internal/platform/error.go
  • internal/platform/host.go
  • internal/platform/host_test.go
  • internal/platform/inventory.go
  • internal/platform/inventory_test.go
  • internal/platform/staging.go
  • internal/policystate/policystate.go
  • internal/skillpolicy/overlay.go
  • internal/skillpolicy/resolver.go
  • internal/skillpolicy/resolver_test.go

Comment thread cmd/config/show.go Outdated
Comment thread cmd/presentation.go Outdated
Comment thread cmd/prune.go Outdated
Comment thread cmd/skill_customization_test.go
Comment thread cmd/skill_customization_test.go Outdated
Comment thread internal/platform/inventory_test.go
Comment thread internal/platform/inventory.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/skillpolicy/resolver_test.go (2)

69-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid calling topLevel(t, got) twice in error assertions.

The pattern !equalStrings(topLevel(t, got), want) followed by t.Errorf("top level = %v, want %v", topLevel(t, got), want) invokes topLevel (and its fs.ReadDir + sort) twice. Extract to a local variable for clarity and to avoid redundant I/O:

-	if want := []string{"lark-a", "lark-b", "lark-shared"}; !equalStrings(topLevel(t, got), want) {
-		t.Errorf("top level = %v, want %v", topLevel(t, got), want)
+	gotNames := topLevel(t, got)
+	if want := []string{"lark-a", "lark-b", "lark-shared"}; !equalStrings(gotNames, want) {
+		t.Errorf("top level = %v, want %v", gotNames, want)

This applies to all 9 test functions using this pattern.

Also applies to: 80-94, 96-106, 108-120, 122-132, 134-148, 153-166, 169-177, 181-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skillpolicy/resolver_test.go` around lines 69 - 78, Store the result
of topLevel(t, got) in a local variable before the equality check, then reuse
that variable in the failure message. Apply this change to all nine affected
test functions, including TestResolve_NoSpecs_ReturnsBaseUnchanged, to avoid
duplicate directory reads and sorting.

263-273: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use slices.Equal here equalStrings is redundant in Go 1.23; replace the call sites with slices.Equal and drop the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skillpolicy/resolver_test.go` around lines 263 - 273, Replace all
call sites of equalStrings in resolver_test.go with slices.Equal, adding the
slices import if needed, then remove the redundant equalStrings helper function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/skillpolicy/resolver_test.go`:
- Around line 69-78: Store the result of topLevel(t, got) in a local variable
before the equality check, then reuse that variable in the failure message.
Apply this change to all nine affected test functions, including
TestResolve_NoSpecs_ReturnsBaseUnchanged, to avoid duplicate directory reads and
sorting.
- Around line 263-273: Replace all call sites of equalStrings in
resolver_test.go with slices.Equal, adding the slices import if needed, then
remove the redundant equalStrings helper function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e579f44f-b98a-4a34-b87d-0bf9532afc68

📥 Commits

Reviewing files that changed from the base of the PR and between efa636f and 965c771.

📒 Files selected for processing (46)
  • cmd/build.go
  • cmd/config/config_test.go
  • cmd/config/plugins.go
  • cmd/config/plugins_test.go
  • cmd/config/show.go
  • cmd/flag_gate.go
  • cmd/platform_bootstrap.go
  • cmd/platform_bootstrap_test.go
  • cmd/platform_guards.go
  • cmd/presentation.go
  • cmd/presentation_test.go
  • cmd/prune.go
  • cmd/prune_test.go
  • cmd/root.go
  • cmd/root_risk_help_test.go
  • cmd/service/affordance_test.go
  • cmd/skill_customization_test.go
  • errs/subtypes.go
  • extension/platform/README.md
  • extension/platform/builder.go
  • extension/platform/builder_test.go
  • extension/platform/capabilities.go
  • extension/platform/registrar.go
  • extension/platform/rule.go
  • extension/platform/skillsoverlay.go
  • internal/auth/errors.go
  • internal/auth/hint_gate_test.go
  • internal/client/client.go
  • internal/client/hint_gate_test.go
  • internal/cmdpolicy/active.go
  • internal/cmdpolicy/aggregation_test.go
  • internal/cmdpolicy/apply.go
  • internal/cmdpolicy/denial.go
  • internal/cmdpolicy/diagnostic.go
  • internal/cmdpolicy/engine.go
  • internal/cmdpolicy/source_label_test.go
  • internal/platform/error.go
  • internal/platform/host.go
  • internal/platform/host_test.go
  • internal/platform/inventory.go
  • internal/platform/inventory_test.go
  • internal/platform/staging.go
  • internal/policystate/policystate.go
  • internal/skillpolicy/overlay.go
  • internal/skillpolicy/resolver.go
  • internal/skillpolicy/resolver_test.go
✅ Files skipped from review due to trivial changes (1)
  • extension/platform/skillsoverlay.go
🚧 Files skipped from review as they are similar to previous changes (42)
  • internal/auth/hint_gate_test.go
  • cmd/config/show.go
  • internal/platform/error.go
  • cmd/prune.go
  • cmd/config/plugins.go
  • internal/cmdpolicy/source_label_test.go
  • internal/client/client.go
  • internal/policystate/policystate.go
  • cmd/config/plugins_test.go
  • extension/platform/capabilities.go
  • internal/cmdpolicy/engine.go
  • internal/auth/errors.go
  • internal/cmdpolicy/active.go
  • internal/cmdpolicy/diagnostic.go
  • cmd/config/config_test.go
  • cmd/root_risk_help_test.go
  • internal/platform/inventory_test.go
  • internal/platform/host_test.go
  • errs/subtypes.go
  • internal/platform/staging.go
  • internal/client/hint_gate_test.go
  • cmd/prune_test.go
  • extension/platform/registrar.go
  • cmd/skill_customization_test.go
  • cmd/platform_guards.go
  • internal/platform/host.go
  • extension/platform/builder.go
  • internal/cmdpolicy/apply.go
  • cmd/platform_bootstrap.go
  • cmd/service/affordance_test.go
  • extension/platform/README.md
  • cmd/platform_bootstrap_test.go
  • extension/platform/builder_test.go
  • internal/cmdpolicy/denial.go
  • cmd/presentation_test.go
  • internal/platform/inventory.go
  • extension/platform/rule.go
  • cmd/presentation.go
  • internal/skillpolicy/resolver.go
  • internal/cmdpolicy/aggregation_test.go
  • cmd/build.go
  • internal/skillpolicy/overlay.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/errclass/hint_gate_test.go`:
- Around line 21-27: Remove the st != errs.SubtypeUserUnauthorized exemption
from the condition in the PermissionHint default-build test, so every listed
subtype must produce a non-empty hint containing "auth login"; simplify the
assertion while preserving the existing diagnostic error message.

In `@internal/hook/install.go`:
- Around line 110-114: Add a regression test in install_test.go covering Install
with a command marked by cmdpolicy.AnnotationFrameworkMeta; verify the
framework-meta command bypasses the plugin wrapper chain while preserving the
underlying result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 887f2f85-ae3c-4ad6-ba7a-94b64a01a5f8

📥 Commits

Reviewing files that changed from the base of the PR and between 965c771 and 547a35a.

📒 Files selected for processing (52)
  • cmd/build.go
  • cmd/config/config_test.go
  • cmd/config/plugins.go
  • cmd/config/plugins_test.go
  • cmd/config/show.go
  • cmd/flag_gate.go
  • cmd/platform_bootstrap.go
  • cmd/platform_bootstrap_test.go
  • cmd/platform_guards.go
  • cmd/presentation.go
  • cmd/presentation_test.go
  • cmd/prune.go
  • cmd/prune_test.go
  • cmd/root.go
  • cmd/root_risk_help_test.go
  • cmd/service/affordance_test.go
  • cmd/skill_customization_test.go
  • errs/subtypes.go
  • extension/platform/README.md
  • extension/platform/builder.go
  • extension/platform/builder_test.go
  • extension/platform/capabilities.go
  • extension/platform/registrar.go
  • extension/platform/registrar_compat_test.go
  • extension/platform/rule.go
  • extension/platform/skillsoverlay.go
  • internal/auth/errors.go
  • internal/auth/hint_gate_test.go
  • internal/client/client.go
  • internal/client/hint_gate_test.go
  • internal/cmdpolicy/active.go
  • internal/cmdpolicy/aggregation_test.go
  • internal/cmdpolicy/apply.go
  • internal/cmdpolicy/denial.go
  • internal/cmdpolicy/diagnostic.go
  • internal/cmdpolicy/engine.go
  • internal/cmdpolicy/source_label_test.go
  • internal/core/notconfigured.go
  • internal/core/notconfigured_hint_gate_test.go
  • internal/errclass/classify.go
  • internal/errclass/hint_gate_test.go
  • internal/hook/install.go
  • internal/platform/error.go
  • internal/platform/host.go
  • internal/platform/host_test.go
  • internal/platform/inventory.go
  • internal/platform/inventory_test.go
  • internal/platform/staging.go
  • internal/policystate/policystate.go
  • internal/skillpolicy/overlay.go
  • internal/skillpolicy/resolver.go
  • internal/skillpolicy/resolver_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/cmdpolicy/diagnostic.go
🚧 Files skipped from review as they are similar to previous changes (45)
  • cmd/root_risk_help_test.go
  • extension/platform/skillsoverlay.go
  • internal/auth/hint_gate_test.go
  • internal/cmdpolicy/denial.go
  • cmd/config/show.go
  • internal/policystate/policystate.go
  • cmd/platform_guards.go
  • errs/subtypes.go
  • internal/cmdpolicy/engine.go
  • cmd/prune.go
  • extension/platform/rule.go
  • extension/platform/registrar.go
  • internal/cmdpolicy/active.go
  • internal/auth/errors.go
  • cmd/config/plugins.go
  • internal/client/hint_gate_test.go
  • internal/platform/error.go
  • cmd/prune_test.go
  • internal/client/client.go
  • cmd/platform_bootstrap_test.go
  • extension/platform/builder_test.go
  • cmd/service/affordance_test.go
  • extension/platform/builder.go
  • cmd/flag_gate.go
  • internal/cmdpolicy/aggregation_test.go
  • internal/platform/inventory.go
  • internal/cmdpolicy/source_label_test.go
  • cmd/config/config_test.go
  • cmd/config/plugins_test.go
  • internal/platform/staging.go
  • cmd/presentation.go
  • internal/cmdpolicy/apply.go
  • internal/platform/host_test.go
  • cmd/platform_bootstrap.go
  • extension/platform/README.md
  • internal/skillpolicy/overlay.go
  • cmd/build.go
  • internal/platform/host.go
  • extension/platform/capabilities.go
  • internal/platform/inventory_test.go
  • internal/skillpolicy/resolver_test.go
  • cmd/root.go
  • internal/skillpolicy/resolver.go
  • cmd/presentation_test.go
  • cmd/skill_customization_test.go

Comment thread internal/errclass/hint_gate_test.go
Comment thread internal/hook/install.go Outdated
@evandance
evandance force-pushed the feat/plugin-trim branch 11 times, most recently from 9198dbe to aeaed08 Compare July 11, 2026 12:28
@evandance
evandance force-pushed the feat/plugin-trim branch 4 times, most recently from 5199518 to fce977d Compare July 28, 2026 06:36
@github-actions github-actions Bot added the domain/ccm PR touches the ccm domain label Jul 28, 2026
@evandance
evandance force-pushed the feat/plugin-trim branch 2 times, most recently from 8e33cc0 to e149df8 Compare July 28, 2026 11:48
@fangshuyu-768 fangshuyu-768 removed the domain/ccm PR touches the ccm domain label Jul 29, 2026
@github-actions github-actions Bot added the domain/ccm PR touches the ccm domain label Jul 29, 2026
@github-actions github-actions Bot added domain/calendar PR touches the calendar domain domain/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain labels Jul 29, 2026
@evandance
evandance force-pushed the feat/plugin-trim branch 3 times, most recently from d37d1ae to 05d26a5 Compare July 29, 2026 09:40
@fangshuyu-768 fangshuyu-768 removed the domain/ccm PR touches the ccm domain label Jul 30, 2026
@evandance evandance removed enhancement New feature or request domain/im PR touches the im domain domain/calendar PR touches the calendar domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain labels Jul 30, 2026
@github-actions github-actions Bot added domain/calendar PR touches the calendar domain domain/ccm PR touches the ccm domain domain/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain labels Jul 30, 2026
Add host-level opt-in concealment for plugin-restricted commands while keeping Restrict enforcement and the established legacy and YAML behavior unchanged.

Compose external wrapper-owned embedded skill trees with fail-closed Base, Allow, Remove, Overlay, and structured reference remaps. Retire CLI-owned dead affordances across help, completion, flags, notices, diagnostics, and recovery.

Keep presentation and skill state build-local and single-sourced, preserve typed producer errors, and cover source compatibility, multi-build isolation, failure paths, and real external fork integrations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/calendar PR touches the calendar domain domain/ccm PR touches the ccm domain domain/im PR touches the im domain domain/mail PR touches the mail domain domain/vc PR touches the vc domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants