Skip to content

Commit 93dcd49

Browse files
author
SqlRush
committed
Load settings-sourced marketplace plugins
1 parent e814e55 commit 93dcd49

6 files changed

Lines changed: 203 additions & 7 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ M8 补充:headless `/plugin list|status` 现在会按 `enabledPlugins` 的 tru
273273

274274
M8 补充:`enabledPlugins` 的 disabled 状态现在会过滤本地 plugin manifest 产生的 slash command、Skill tool prompt、agent/MCP/output-style/status/stream-init metadata,`/plugin disable <name>` 写入配置后不再只影响展示层;marketplace 下载/安装、版本解析、动态 refresh lifecycle 和完整 UI 管理仍待补齐。
275275

276-
M8 补充:`internal/plugins` 新增 marketplace policy resolver,会根据 `blockedMarketplaces` 优先拒绝来源,并在 `strictKnownMarketplaces` 非空时只允许 strict allowlist 内的 marketplace;`/plugin marketplaces` 现在展示每个 extra/strict/blocked marketplace 的 allow/block decision;本地 plugin manifest 可声明 `marketplace`/`marketplaceName`/`marketplace_name`/`source.name`,带 settings 的 plugin loader 会按 marketplace policy 过滤 command/skill/agent/MCP/output-style/hook 激活路径`/plugin show/search` 会标注 blocked reason。真实 marketplace 下载/安装/cache lifecycle 仍待补齐。
276+
M8 补充:`internal/plugins` 新增 marketplace policy resolver,会根据 `blockedMarketplaces` 优先拒绝来源,并在 `strictKnownMarketplaces` 非空时只允许 strict allowlist 内的 marketplace;`/plugin marketplaces` 现在展示每个 extra/strict/blocked marketplace 的 allow/block decision;本地 plugin manifest 可声明 `marketplace`/`marketplaceName`/`marketplace_name`/`source.name`,带 settings 的 plugin loader 会按 marketplace policy 过滤 command/skill/agent/MCP/output-style/hook 激活路径`extraKnownMarketplaces.<name>.source.source=settings``plugins` 本地 root 也会进入 plugin load path 并继承 marketplace 名称;`/plugin show/search` 会标注 blocked reason。真实 marketplace 下载/安装/cache lifecycle 仍待补齐。
277277

278278
M8 补充:headless `/config output-style <name>` 现在会校验可用 output style、写入用户 `settings.json``outputStyle`,并同步当前 runner settings;完整 TUI picker 和状态栏附件仍待补齐。
279279

@@ -733,7 +733,7 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
733733
本轮补充:`cmd/claude --help` 现在沿用 Go flag usage 输出并成功退出;`cmd/claude --cwd` 现在会设置 bootstrap working directory,影响 scaffold 输出、project settings、tool cwd 和 transcript path;`cmd/claude --print/-p` 现在接入真实 `conversation.Runner.RunTurn` 单轮 headless 路径,可从参数或 stdin 读取 prompt,读取 `ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL`/`ANTHROPIC_MODEL`/`CLAUDE_MODEL`/settings model,解析模型别名,装配 builtin tools、settings-derived permission engine、settings-derived MCP config,并把最终 assistant text 写到 stdout;`--mcp-config`/`--mcpConfig` 现在会把指定 JSON 文件中的 `mcpServers` 合入 headless MCP local settings;`--input-format`/`--inputFormat` text/json/stream-json 现在有基础输入解析,支持 JSON prompt/user message 和 NDJSON user event;`--max-turns`/`--maxTurns` 现在会限制 headless tool-use loop 轮数,`--max-tokens`/`--maxTokens` 和 `--max-turns`/`--maxTurns` 都会拒绝负值;`--output-format`/`--outputFormat` json 现在会输出基础 result envelope,`stream-json` 会先输出基础 `system/init` 事件,再输出 NDJSON event stream 并以 result 行收尾,包含 result text、session id、assistant message、stop reason、model、usage 和 tool results;headless setup/resume/RunTurn 错误现在会在 JSON 模式输出 `subtype:error` result,在 stream-json 模式输出 `type:error` event;`--permission-mode`/`--permissionMode`、`--dangerously-skip-permissions`/`--dangerouslySkipPermissions`、`--system-prompt`/`--systemPrompt` 和 `--append-system-prompt`/`--appendSystemPrompt` 现在也有 CLI 接线或 camelCase alias;`--stream --output-format stream-json` 现在还会透出 raw Anthropic streaming events,包括 text delta;headless `--resume <session-id-or-jsonl>` 和 `--continue` 现在会加载当前项目 transcript chain 作为 history,并把新回合追加回同一个 transcript;`--allowedTools`/`--allowed-tools` 和 `--disallowedTools`/`--disallowed-tools` 现在会作为 CLI permission rules 合入 headless permission engine;`--add-dir`/`--addDir` 现在会作为 CLI additional working directory 合入 headless permission context。
734734
本轮补充:settings 文件读取现在有 path-keyed cache,按 size/mode/mtime 指纹复用内容并提供 cache reset;新增 settings change detector,可对 settings 文件快照区分 created/modified/deleted,并在检测到变化时清空 settings 文件缓存。
735735

736-
当前状态:bootstrap/config/auth/model 基础已完成;settings 已有 merge、managed policy(含本地 file/drop-in、MDM/registry、可选 remote GET source、turn-start remote refresh/app-state propagation 和 daemon heartbeat remote refresh)、settings file cache/change detector、turn-start local settings reload/app-state propagation、JSON Schema generation、marketplace source union 基础 validation、marketplace policy resolver/visibility/local manifest load enforcement 和 `strictPluginOnlyCustomization` 基础 enforcement;CLI 已有 `--version`、`--help` 成功退出、`--cwd` working directory override、scaffold settings 校验、基础 `--print` headless 单轮执行路径、基础 `--mcp-config`、基础 input-format text/json/stream-json、`--max-turns` tool loop 限制、基础 JSON result/error 输出、基础 stream-json init/event/error 输出、raw streaming event 透传、headless resume/continue transcript 接线、system prompt flags、dangerously-skip-permissions、常见 camelCase flag aliases、CLI allow/deny tool rules 和 CLI add-dir additional working directory context,但完整参数矩阵、交互 TUI 主循环、resume picker/UI、官方 SDK NDJSON/control protocol、settings background watcher/continuous app-state sync、remote managed 非 daemon 后台 refresh 和官方 stdout/stderr/exit-code parity 仍未完整兼容 CC。
736+
当前状态:bootstrap/config/auth/model 基础已完成;settings 已有 merge、managed policy(含本地 file/drop-in、MDM/registry、可选 remote GET source、turn-start remote refresh/app-state propagation 和 daemon heartbeat remote refresh)、settings file cache/change detector、turn-start local settings reload/app-state propagation、JSON Schema generation、marketplace source union 基础 validation、marketplace policy resolver/visibility/local+settings-sourced manifest load enforcement 和 `strictPluginOnlyCustomization` 基础 enforcement;CLI 已有 `--version`、`--help` 成功退出、`--cwd` working directory override、scaffold settings 校验、基础 `--print` headless 单轮执行路径、基础 `--mcp-config`、基础 input-format text/json/stream-json、`--max-turns` tool loop 限制、基础 JSON result/error 输出、基础 stream-json init/event/error 输出、raw streaming event 透传、headless resume/continue transcript 接线、system prompt flags、dangerously-skip-permissions、常见 camelCase flag aliases、CLI allow/deny tool rules 和 CLI add-dir additional working directory context,但完整参数矩阵、交互 TUI 主循环、resume picker/UI、官方 SDK NDJSON/control protocol、settings background watcher/continuous app-state sync、remote managed 非 daemon 后台 refresh 和官方 stdout/stderr/exit-code parity 仍未完整兼容 CC。
737737

738738
### M3: API Client And Conversation Loop
739739

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ test/parity/ # golden tests against TS/official behavior
143143
- 本轮补充:`strictPluginOnlyCustomization` 已开始按 policy settings 生效,当前覆盖已实现的 `skills``mcp``hooks` 和 agent-metadata surfaces:锁定后 user/project/local skills 与 legacy commands 不加载,manual MCP server sources 不加载但 plugin/admin policy 仍保留,settings hooks 只保留 policy hooks 且 plugin hooks 继续运行;`agents` 锁定时非 plugin/bundled/admin prompt command 的 `agent``context: fork``effort` frontmatter metadata 会被清除。完整 forked agent runtime/UI edge cases 仍按缺口追踪。
144144
- 本轮补充:settings 文件读取现在有 path-keyed cache,按 size/mode/mtime 指纹复用内容并提供 `ResetSettingsCache`;新增 settings change detector,可对 settings 文件快照区分 created/modified/deleted,并在检测到变化时清空 settings 文件缓存;Runner turn-start 会在检测到 user/project/local settings 文件变化时重载 merged settings 并同步刷新 plugin MCP server app-state。完整后台 watcher 和 remote managed 非 daemon 后台 refresh 生命周期仍按缺口追踪。
145145
- 本轮补充:新增 settings JSON Schema generation,`SettingsJSONSchema()` 会从 `contracts.Settings` 反射生成 draft 2020-12 schema,补强 `$schema` const、permission mode enum、`strictPluginOnlyCustomization` union 和 login method enum;`/config show schema` 现在可展示 schema ID、draft、字段数和生成大小,完整 Zod message parity 仍按缺口追踪。
146-
- 本轮补充:settings validation 现在覆盖官方 marketplace source union 的基础语义,适用于 `extraKnownMarketplaces[].source``strictKnownMarketplaces[]``blockedMarketplaces[]`:校验 source discriminator、各 source 类型必填字段、URL source 绝对 URL、headers string record、github/git sparsePaths string array、settings-source plugins array、settings-source name 安全/保留名,并继续覆盖 inline marketplace key/name 一致性;`internal/plugins` 新增 marketplace policy resolver,blocked settings 优先、strict allowlist 非空时拒绝未列入来源,`/plugin marketplaces` 会展示 allow/block decision;本地 plugin manifest 可声明 `marketplace`/`marketplaceName`/`marketplace_name`/`source.name`,带 settings 的 plugin loader 会对这些来源执行 marketplace policy,`/plugin show/search` 会标注 blocked reason。真实 marketplace download/install/cache enforcement 仍按缺口追踪。
146+
- 本轮补充:settings validation 现在覆盖官方 marketplace source union 的基础语义,适用于 `extraKnownMarketplaces[].source`、`strictKnownMarketplaces[]` 和 `blockedMarketplaces[]`:校验 source discriminator、各 source 类型必填字段、URL source 绝对 URL、headers string record、github/git sparsePaths string array、settings-source plugins array、settings-source name 安全/保留名,并继续覆盖 inline marketplace key/name 一致性;`internal/plugins` 新增 marketplace policy resolver,blocked settings 优先、strict allowlist 非空时拒绝未列入来源,`/plugin marketplaces` 会展示 allow/block decision;本地 plugin manifest 可声明 `marketplace`/`marketplaceName`/`marketplace_name`/`source.name`,带 settings 的 plugin loader 会对这些来源执行 marketplace policy,`extraKnownMarketplaces.<name>.source.source=settings` 的 `plugins` 本地 root 也会进入 plugin load path 并继承 marketplace 名称,`/plugin show/search` 会标注 blocked reason。真实 marketplace download/install/cache enforcement 仍按缺口追踪。
147147

148148
### M4: Tool framework、permissions、sandbox
149149

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ The following items remain incomplete and must not be treated as done:
108108
- Full tool execution parity gaps that remain: complete hook runtime policy beyond the current settings command hook path, MCP elicitation, complete SDK control event surface beyond current progress NDJSON, mid-call cancellation for concrete tools, background task behavior, telemetry, schema-not-sent hints, and concrete tool-specific semantics.
109109
- Complete Anthropic API parity gaps that remain: dynamic beta-header latching for any remaining provider-specific feature gates beyond prompt-cache/cache-editing/strict-output/1M-context requests, ant-only dump gating and remote `/issue` submission integration beyond the current local context summary, full official cost tracker/session restore lifecycle beyond current transcript usage restore, post-handshake streaming failure semantics, deeper gateway/proxy auth and routing behavior beyond static custom headers, first-party/Bedrock/Vertex/Foundry client setup, deeper OAuth account/profile lifecycle beyond request-time refresh retry, fast-mode retry/cooldown semantics, persistent unattended retry heartbeats, full prompt-cache editing lifecycle, and provider-specific cache behavior.
110110
- Full conversation/query loop: stop hooks, compact/auto-compact, token budget escalation, resume, SDK JSON/NDJSON control events, status updates, rate-limit handling, model switch breadcrumbs, side questions.
111-
- Full settings parity gaps that remain: complete Zod-equivalent validation messages, full remote managed-settings watcher/non-daemon background refresh lifecycle beyond current turn-start and daemon-heartbeat refresh, remaining forked agent/frontmatter execution and UI edge cases beyond current policy metadata filtering, marketplace download/install/cache enforcement beyond current local manifest load enforcement, and background watcher/continuous app-state sync beyond current turn-start local settings reload.
111+
- Full settings parity gaps that remain: complete Zod-equivalent validation messages, full remote managed-settings watcher/non-daemon background refresh lifecycle beyond current turn-start and daemon-heartbeat refresh, remaining forked agent/frontmatter execution and UI edge cases beyond current policy metadata filtering, marketplace download/install/cache enforcement beyond current local and settings-sourced manifest load enforcement, and background watcher/continuous app-state sync beyond current turn-start local settings reload.
112112
- Full session/history parity gaps that remain: large-file optimized transcript loading, preserved-segment edge cases beyond current relink/prune support, content-replacement feature-flag/runtime override and inherited subagent gap-fill details, complete async prompt-history lifecycle parity beyond current lock/buffer/undo paths, full pasted-image processing/runtime integration beyond current prompt image-cache/image-block/metadata path, remaining remote-history edge cases, sidechain/subagent transcript layout, and all session metadata entry types.
113113

114114
## Current Verification

internal/conversation/run_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4716,6 +4716,61 @@ func TestRunnerPluginShowReportsLocalPluginDetails(t *testing.T) {
47164716
}
47174717
}
47184718

4719+
func TestRunnerPluginListIncludesSettingsMarketplacePlugins(t *testing.T) {
4720+
client := &fakeClient{}
4721+
root := t.TempDir()
4722+
cwd := filepath.Join(root, "project")
4723+
pluginDir := filepath.Join(root, "marketplace-plugin")
4724+
if err := os.MkdirAll(cwd, 0o755); err != nil {
4725+
t.Fatal(err)
4726+
}
4727+
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
4728+
t.Fatal(err)
4729+
}
4730+
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(`{
4731+
"name": "market-demo",
4732+
"commands": [{"name": "market:deploy", "description": "Deploy marketplace plugin", "prompt": "Deploy."}]
4733+
}`), 0o644); err != nil {
4734+
t.Fatal(err)
4735+
}
4736+
runner := Runner{
4737+
Client: client,
4738+
SessionID: "sess_plugin_settings_marketplace",
4739+
WorkingDirectory: cwd,
4740+
MCP: &MCPConfig{UserSettings: contracts.Settings{
4741+
ExtraKnownMarketplaces: map[string]any{
4742+
"team": map[string]any{"source": map[string]any{
4743+
"source": "settings",
4744+
"name": "team",
4745+
"plugins": []any{pluginDir},
4746+
}},
4747+
},
4748+
StrictKnownMarketplaces: []any{"team"},
4749+
}},
4750+
}
4751+
4752+
result, err := runner.RunTurn(context.Background(), nil, messages.UserText("/plugin list"))
4753+
if err != nil {
4754+
t.Fatal(err)
4755+
}
4756+
if len(client.requests) != 0 {
4757+
t.Fatalf("model should not be queried, requests = %#v", client.requests)
4758+
}
4759+
text := result.Messages[1].Content[0].Text
4760+
for _, want := range []string{
4761+
"Local plugin manifests: 1",
4762+
"Registered plugin commands: 1",
4763+
"Local plugins:",
4764+
"- market-demo",
4765+
"Plugin commands:",
4766+
"- /market:deploy",
4767+
} {
4768+
if !strings.Contains(text, want) {
4769+
t.Fatalf("plugin list missing %q: %q", want, text)
4770+
}
4771+
}
4772+
}
4773+
47194774
func TestRunnerPluginShowReportsDisabledLocalPlugin(t *testing.T) {
47204775
repo := filepath.Join(t.TempDir(), "repo")
47214776
cwd := filepath.Join(repo, "pkg")

internal/plugins/loader.go

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,20 @@ type skillManifest struct {
108108
Description string `json:"description"`
109109
}
110110

111+
type pluginRootEntry struct {
112+
Root string
113+
Marketplace string
114+
}
115+
111116
func LoadPluginDirs(roots []string) []LoadedPlugin {
117+
return loadPluginRootEntries(pluginRootEntriesFromRoots(roots))
118+
}
119+
120+
func loadPluginRootEntries(roots []pluginRootEntry) []LoadedPlugin {
112121
out := make([]LoadedPlugin, 0, len(roots))
113122
seen := map[string]struct{}{}
114-
for _, root := range roots {
115-
root = cleanAbs(root)
123+
for _, entry := range roots {
124+
root := cleanAbs(entry.Root)
116125
key := normalizePath(root)
117126
if _, ok := seen[key]; ok {
118127
continue
@@ -122,13 +131,18 @@ func LoadPluginDirs(roots []string) []LoadedPlugin {
122131
if err != nil {
123132
continue
124133
}
134+
if strings.TrimSpace(entry.Marketplace) != "" {
135+
plugin.Marketplace = strings.TrimSpace(entry.Marketplace)
136+
}
125137
out = append(out, plugin)
126138
}
127139
return out
128140
}
129141

130142
func LoadPluginDirsWithSettings(roots []string, settings contracts.Settings) []LoadedPlugin {
131-
return FilterPluginsWithSettings(LoadPluginDirs(roots), settings)
143+
entries := pluginRootEntriesFromRoots(roots)
144+
entries = append(entries, marketplacePluginRootEntries(settings)...)
145+
return FilterPluginsWithSettings(loadPluginRootEntries(entries), settings)
132146
}
133147

134148
func FilterPluginsWithSettings(plugins []LoadedPlugin, settings contracts.Settings) []LoadedPlugin {
@@ -267,6 +281,86 @@ func pluginMarketplaceName(parsed manifest) string {
267281
return firstNonEmpty(parsed.Marketplace, parsed.MarketplaceName, parsed.MarketplaceSnake, marketplaceNameFromAny(parsed.Source))
268282
}
269283

284+
func pluginRootEntriesFromRoots(roots []string) []pluginRootEntry {
285+
entries := make([]pluginRootEntry, 0, len(roots))
286+
for _, root := range roots {
287+
if strings.TrimSpace(root) == "" {
288+
continue
289+
}
290+
entries = append(entries, pluginRootEntry{Root: root})
291+
}
292+
return entries
293+
}
294+
295+
func marketplacePluginRootEntries(settings contracts.Settings) []pluginRootEntry {
296+
if len(settings.ExtraKnownMarketplaces) == 0 {
297+
return nil
298+
}
299+
names := sortedMarketplaceMapKeys(settings.ExtraKnownMarketplaces)
300+
var entries []pluginRootEntry
301+
for _, name := range names {
302+
source, ok := settingsMarketplaceSource(settings.ExtraKnownMarketplaces[name])
303+
if !ok {
304+
continue
305+
}
306+
if sourceType, _ := source["source"].(string); strings.TrimSpace(sourceType) != "settings" {
307+
continue
308+
}
309+
marketplace := firstNonEmpty(stringFromAnyMap(source, "name"), name)
310+
plugins, _ := source["plugins"].([]any)
311+
for _, rawPlugin := range plugins {
312+
root := settingsMarketplacePluginRoot(rawPlugin)
313+
if root == "" {
314+
continue
315+
}
316+
entries = append(entries, pluginRootEntry{Root: root, Marketplace: marketplace})
317+
}
318+
}
319+
return entries
320+
}
321+
322+
func settingsMarketplaceSource(raw any) (map[string]any, bool) {
323+
entry, ok := raw.(map[string]any)
324+
if !ok {
325+
return nil, false
326+
}
327+
source, ok := entry["source"].(map[string]any)
328+
return source, ok
329+
}
330+
331+
func settingsMarketplacePluginRoot(raw any) string {
332+
if text, ok := raw.(string); ok {
333+
return strings.TrimSpace(text)
334+
}
335+
item, ok := raw.(map[string]any)
336+
if !ok {
337+
return ""
338+
}
339+
for _, key := range []string{"path", "root", "dir", "directory"} {
340+
if value := stringFromAnyMap(item, key); value != "" {
341+
return value
342+
}
343+
}
344+
return ""
345+
}
346+
347+
func stringFromAnyMap(values map[string]any, key string) string {
348+
value, _ := values[key].(string)
349+
return strings.TrimSpace(value)
350+
}
351+
352+
func sortedMarketplaceMapKeys(values map[string]any) []string {
353+
keys := make([]string, 0, len(values))
354+
for key := range values {
355+
key = strings.TrimSpace(key)
356+
if key != "" {
357+
keys = append(keys, key)
358+
}
359+
}
360+
sort.Strings(keys)
361+
return keys
362+
}
363+
270364
func LoadMCPServers(roots []string) map[string]contracts.MCPServer {
271365
return LoadMCPServersWithSettings(roots, contracts.Settings{})
272366
}

0 commit comments

Comments
 (0)