From 6b1c8859ea2566511a579824431dbd2da27533ae Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 09:16:38 +0800 Subject: [PATCH 01/13] refactor(deplugin): fold model-policy into the core capacity catalog The release-pinned official model capacity catalog and its product-scoped resolver now live in backend/internal/service instead of the model-policy plugin process. Capacity lookups resolve the official catalog in process and merge any remaining plugin catalog (Cindy) with the plugin manager rule: different capacities are ambiguous, otherwise the later equal entry wins. The admin catalog browser becomes a native settings panel backed by GET /api/v1/admin/settings/model-context-catalog. The plugin module, its bundle entry, go.mod replace and process contract test are removed; its three catalog tests move to the service package. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/test_first_party_plugins.sh | 3 +- backend/go.mod | 3 - .../internal/handler/admin/setting_handler.go | 10 ++ backend/internal/server/routes/admin.go | 1 + .../service/model_context_capacity_catalog.go | 10 +- ...text_capacity_catalog_test_fixture_test.go | 13 -- .../service/model_context_official_catalog.go | 127 ++++++++---------- .../model_context_official_catalog_data.go | 16 +-- .../model_context_official_catalog_test.go | 77 +++++++++++ .../internal/service/plugin_catalog_test.go | 9 +- .../service/plugin_extension_runtime_test.go | 27 ---- backend/internal/testextensions/fixtures.go | 5 +- frontend/src/api/admin/settings.ts | 36 +++++ .../admin/OfficialModelCatalogPanel.vue | 82 +++++++++++ .../src/i18n/locales/en/admin/settings.ts | 11 ++ .../src/i18n/locales/zh/admin/settings.ts | 11 ++ frontend/src/views/admin/SettingsView.vue | 2 + plugins/bundle.source.json | 1 - plugins/model-policy/catalog/module_test.go | 70 ---------- plugins/model-policy/cmd/plugin/main.go | 12 -- plugins/model-policy/go.mod | 24 ---- plugins/model-policy/go.sum | 83 ------------ plugins/model-policy/manifest.source.json | 30 ----- plugins/model-policy/ui/assets/app.js | 32 ----- plugins/model-policy/ui/assets/styles.css | 13 -- plugins/model-policy/ui/index.html | 4 - 26 files changed, 303 insertions(+), 409 deletions(-) delete mode 100644 backend/internal/service/model_context_capacity_catalog_test_fixture_test.go rename plugins/model-policy/catalog/module.go => backend/internal/service/model_context_official_catalog.go (51%) rename plugins/model-policy/catalog/catalog_data.go => backend/internal/service/model_context_official_catalog_data.go (99%) create mode 100644 backend/internal/service/model_context_official_catalog_test.go create mode 100644 frontend/src/components/admin/OfficialModelCatalogPanel.vue delete mode 100644 plugins/model-policy/catalog/module_test.go delete mode 100644 plugins/model-policy/cmd/plugin/main.go delete mode 100644 plugins/model-policy/go.mod delete mode 100644 plugins/model-policy/go.sum delete mode 100644 plugins/model-policy/manifest.source.json delete mode 100644 plugins/model-policy/ui/assets/app.js delete mode 100644 plugins/model-policy/ui/assets/styles.css delete mode 100644 plugins/model-policy/ui/index.html diff --git a/.github/scripts/test_first_party_plugins.sh b/.github/scripts/test_first_party_plugins.sh index 1e8ba8b1934a..81f0c2adc59d 100644 --- a/.github/scripts/test_first_party_plugins.sh +++ b/.github/scripts/test_first_party_plugins.sh @@ -28,7 +28,6 @@ go build -trimpath -o "$plugin_build_dir/package-plugin" ./cmd/package-plugin -key-id codexrip-plugins-test export SUB2API_EXTENSION_TEST_BINARY="$plugin_build_dir/binaries/codex-runtime" -export SUB2API_MODEL_POLICY_TEST_BINARY="$plugin_build_dir/binaries/model-policy" export SUB2API_EXTENSION_TEST_BUNDLE="$plugin_build_dir/bundle/lock.json" export SUB2API_PROMPT_SKILLS_TEST_BINARY="$plugin_build_dir/binaries/prompt-skills" export SUB2API_ACCOUNT_TOOLS_TEST_BINARY="$plugin_build_dir/binaries/account-tools" @@ -37,4 +36,4 @@ export SUB2API_IMAGE_TOOLS_TEST_BINARY="$plugin_build_dir/binaries/image-tools" export SUB2API_ADMIN_OBSERVABILITY_TEST_BINARY="$plugin_build_dir/binaries/admin-observability" export SUB2API_EXTENSION_TEST_PUBLIC_KEY SUB2API_EXTENSION_TEST_PUBLIC_KEY=$(jq -r .public_key "$plugin_build_dir/publisher.json") -go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestCatalogExtensionRuntimeUsesIndependentProcess|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestImageToolsExtensionRuntimeUsesIndependentProcess|TestObservabilityExtensionRuntimeUsesScopedMetricsBroker|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 +go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestImageToolsExtensionRuntimeUsesIndependentProcess|TestObservabilityExtensionRuntimeUsesScopedMetricsBroker|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 diff --git a/backend/go.mod b/backend/go.mod index d21098adf666..56a7334e627b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,8 +2,6 @@ module github.com/Wei-Shaw/sub2api go 1.27.0 -replace github.com/HTExplicit/sub2api-plugins/modelpolicy => ../plugins/model-policy - replace github.com/HTExplicit/sub2api-plugins/codexruntime => ../plugins/codex-runtime replace github.com/HTExplicit/sub2api-plugins/promptskills => ../plugins/prompt-skills @@ -17,7 +15,6 @@ replace github.com/HTExplicit/sub2api-plugins/imagetools => ../plugins/image-too replace github.com/HTExplicit/sub2api-plugins/adminobservability => ../plugins/admin-observability require ( - github.com/HTExplicit/sub2api-plugins/modelpolicy v0.0.0 github.com/HTExplicit/sub2api-plugins/codexruntime v0.0.0 github.com/HTExplicit/sub2api-plugins/promptskills v0.0.0 github.com/HTExplicit/sub2api-plugins/accounttools v0.0.0 diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 1b94c77fd0ec..ce69e801d026 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -101,6 +101,16 @@ func (h *SettingHandler) SetStepUpDeps(totpService *service.TotpService, userSer // GetSettings 获取所有系统设置 // GET /api/v1/admin/settings +// GetOfficialModelContextCatalog returns the release-pinned official model +// capacity catalog for the read-only admin browser. +// GET /api/v1/admin/settings/model-context-catalog +func (h *SettingHandler) GetOfficialModelContextCatalog(c *gin.Context) { + response.Success(c, gin.H{ + "reference_release": service.GPTContextCapacityReferenceRelease, + "entries": service.OfficialModelCatalogSnapshot(), + }) +} + func (h *SettingHandler) GetSettings(c *gin.Context) { c.Header("Cache-Control", "no-store") settings, err := h.settingService.GetAllSettings(c.Request.Context()) diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index c1fc86e56b60..e06081b4e06e 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -722,6 +722,7 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { { adminSettings.GET("", h.Admin.Setting.GetSettings) adminSettings.PUT("", h.Admin.Setting.UpdateSettings) + adminSettings.GET("/model-context-catalog", h.Admin.Setting.GetOfficialModelContextCatalog) adminSettings.POST("/openai-codex-ticket/proxy-test", h.Admin.Setting.TestCodexTicketProxy) adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection) adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) diff --git a/backend/internal/service/model_context_capacity_catalog.go b/backend/internal/service/model_context_capacity_catalog.go index fa40c599df43..4ab23df41277 100644 --- a/backend/internal/service/model_context_capacity_catalog.go +++ b/backend/internal/service/model_context_capacity_catalog.go @@ -16,17 +16,23 @@ type extensionCatalogProvider struct{ resolver extensionv1.CatalogResolver } var processExtensionCatalog atomic.Pointer[extensionCatalogProvider] func lookupExtensionCatalog(query extensionv1.CatalogQuery) *OfficialModelContextCapacity { + official, err := resolveOfficialModelCatalog(query) + if err != nil { + return nil + } provider := processExtensionCatalog.Load() if provider == nil { - return nil + return official.Entry } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() + // Remaining plugin catalogs (Cindy) still answer through the plugin manager. + // An unavailable enabled catalog keeps the lookup conservative, as before. result, err := provider.resolver.ResolveCatalog(ctx, query) if err != nil { return nil } - return result.Entry + return mergeCatalogMatches(official, result).Entry } func (m *PluginManager) ResolveCatalog(ctx context.Context, query extensionv1.CatalogQuery) (extensionv1.CatalogMatch, error) { if err := ctx.Err(); err != nil { diff --git a/backend/internal/service/model_context_capacity_catalog_test_fixture_test.go b/backend/internal/service/model_context_capacity_catalog_test_fixture_test.go deleted file mode 100644 index 2b1e975ae06e..000000000000 --- a/backend/internal/service/model_context_capacity_catalog_test_fixture_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package service - -// Contract tests use the actual independent catalog module as their in-process -// fixture. Production reaches it only through the enabled plugin process. -import catalog "github.com/HTExplicit/sub2api-plugins/modelpolicy/catalog" - -var officialModelContextCapacityCatalog = catalog.Snapshot() - -const GPTContextCapacityReferenceRelease = catalog.ReferenceRelease -const gptContextCapacityReferenceSource = catalog.ReferenceSource -const gptContextCapacityReferenceVerifiedAt = catalog.ReferenceVerifiedAt - -func init() { processExtensionCatalog.Store(&extensionCatalogProvider{resolver: catalog.New()}) } diff --git a/plugins/model-policy/catalog/module.go b/backend/internal/service/model_context_official_catalog.go similarity index 51% rename from plugins/model-policy/catalog/module.go rename to backend/internal/service/model_context_official_catalog.go index a1611e722876..85c832f1cc41 100644 --- a/plugins/model-policy/catalog/module.go +++ b/backend/internal/service/model_context_official_catalog.go @@ -1,73 +1,17 @@ -package catalog +package service import ( - "context" - "encoding/json" "errors" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "slices" "strings" -) - -const ReferenceRelease = GPTContextCapacityReferenceRelease -const ReferenceSource = gptContextCapacityReferenceSource -const ReferenceVerifiedAt = gptContextCapacityReferenceVerifiedAt - -type Module struct{} -func New() *Module { return &Module{} } -func (m *Module) ValidateConfig(_ context.Context, raw json.RawMessage) (json.RawMessage, error) { - var fields map[string]json.RawMessage - if json.Unmarshal(raw, &fields) != nil || fields == nil || len(fields) != 0 { - return nil, errors.New("model catalog has no configurable fields") - } - return json.RawMessage(`{}`), nil -} -func (m *Module) ApplyConfig(ctx context.Context, raw json.RawMessage) error { - _, err := m.ValidateConfig(ctx, raw) - return err -} -func (m *Module) Status(context.Context) (json.RawMessage, error) { - return json.Marshal(map[string]any{"entries": len(officialModelContextCapacityCatalog), "reference_release": ReferenceRelease}) -} -func (m *Module) Invoke(ctx context.Context, in extensionv1.Invocation) (extensionv1.Result, error) { - var value any - switch { - case in.Capability == extensionv1.CapabilityCatalog && in.Operation == "resolve": - var request extensionv1.CatalogQuery - if json.Unmarshal(in.Payload, &request) != nil { - return extensionv1.Result{}, errors.New("invalid model reference query") - } - match, err := m.ResolveCatalog(ctx, request) - if err != nil { - return extensionv1.Result{}, err - } - value = match - case in.Capability == extensionv1.CapabilityAdmin && in.Operation == "catalog.list": - value = Snapshot() - default: - return extensionv1.Result{}, errors.New("unsupported catalog operation") - } - raw, err := json.Marshal(value) - return extensionv1.Result{Payload: raw}, err -} + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) -func Snapshot() []OfficialModelContextCapacity { - result := make([]OfficialModelContextCapacity, len(officialModelContextCapacityCatalog)) - for i, entry := range officialModelContextCapacityCatalog { - entry.Aliases = slices.Clone(entry.Aliases) - entry.SourceURLs = slices.Clone(entry.SourceURLs) - entry.MatchHosts = slices.Clone(entry.MatchHosts) - entry.MatchAccountModes = slices.Clone(entry.MatchAccountModes) - if entry.Reference != nil { - reference := *entry.Reference - entry.Reference = &reference - } - result[i] = entry - } - return result -} -func (m *Module) ResolveCatalog(_ context.Context, query extensionv1.CatalogQuery) (extensionv1.CatalogMatch, error) { +// resolveOfficialModelCatalog matches candidates against the release-pinned +// official catalog in process (formerly the model-policy plugin). Product +// scoping keeps a self-hosted look-alike ID from inheriting official capacity. +func resolveOfficialModelCatalog(query extensionv1.CatalogQuery) (extensionv1.CatalogMatch, error) { if len(query.Candidates) == 0 || len(query.Candidates) > 16 { return extensionv1.CatalogMatch{}, errors.New("invalid model candidates") } @@ -79,13 +23,13 @@ func (m *Module) ResolveCatalog(_ context.Context, query extensionv1.CatalogQuer } var found *OfficialModelContextCapacity for _, entry := range officialModelContextCapacityCatalog { - if !strings.EqualFold(entry.ModelID, candidate) && !containsFold(entry.Aliases, candidate) { + if !strings.EqualFold(entry.ModelID, candidate) && !officialCatalogContainsFold(entry.Aliases, candidate) { continue } - if !applies(query, entry) { + if !officialCatalogApplies(query, entry) { continue } - if !validCapacity(entry.ContextWindow) && !validCapacity(entry.MaxInputTokens) && !validCapacity(entry.MaxContextWindow) { + if !validOfficialCatalogCapacity(entry.ContextWindow) && !validOfficialCatalogCapacity(entry.MaxInputTokens) && !validOfficialCatalogCapacity(entry.MaxContextWindow) { continue } if found != nil && found.ModelContextCapacity != entry.ModelContextCapacity { @@ -106,8 +50,30 @@ func (m *Module) ResolveCatalog(_ context.Context, query extensionv1.CatalogQuer } return extensionv1.CatalogMatch{}, nil } -func validCapacity(value int64) bool { return value > 0 && value <= extensionv1.MaxModelContextTokens } -func containsFold(values []string, target string) bool { + +// OfficialModelCatalogSnapshot returns a deep copy of the official catalog for +// the admin catalog browser. +func OfficialModelCatalogSnapshot() []OfficialModelContextCapacity { + result := make([]OfficialModelContextCapacity, len(officialModelContextCapacityCatalog)) + for i, entry := range officialModelContextCapacityCatalog { + entry.Aliases = slices.Clone(entry.Aliases) + entry.SourceURLs = slices.Clone(entry.SourceURLs) + entry.MatchHosts = slices.Clone(entry.MatchHosts) + entry.MatchAccountModes = slices.Clone(entry.MatchAccountModes) + if entry.Reference != nil { + reference := *entry.Reference + entry.Reference = &reference + } + result[i] = entry + } + return result +} + +func validOfficialCatalogCapacity(value int64) bool { + return value > 0 && value <= extensionv1.MaxModelContextTokens +} + +func officialCatalogContainsFold(values []string, target string) bool { for _, value := range values { if strings.EqualFold(value, target) { return true @@ -115,13 +81,14 @@ func containsFold(values []string, target string) bool { } return false } -func applies(query extensionv1.CatalogQuery, entry OfficialModelContextCapacity) bool { + +func officialCatalogApplies(query extensionv1.CatalogQuery, entry OfficialModelContextCapacity) bool { secure := query.Scheme == "https" && !query.HasURLCredentials && (query.Port == "" || query.Port == "443") kimiCoding := secure && strings.EqualFold(query.Host, "api.kimi.com") && (query.Path == "/coding" || strings.HasPrefix(query.Path, "/coding/")) if entry.Provider == "kimi" && entry.Product == "coding" && query.Platform != "kimi" && !kimiCoding { return false } - if len(entry.MatchHosts) > 0 && (!secure || !containsFold(entry.MatchHosts, query.Host)) { + if len(entry.MatchHosts) > 0 && (!secure || !officialCatalogContainsFold(entry.MatchHosts, query.Host)) { return false } if len(entry.MatchAccountModes) > 0 { @@ -139,3 +106,23 @@ func applies(query extensionv1.CatalogQuery, entry OfficialModelContextCapacity) } return true } + +// mergeCatalogMatches combines ordered catalog answers with the plugin +// manager's rule: an ambiguous answer, or two different capacities, is +// ambiguous; otherwise the later equal-capacity entry wins. +func mergeCatalogMatches(matches ...extensionv1.CatalogMatch) extensionv1.CatalogMatch { + var resolved extensionv1.CatalogMatch + for _, match := range matches { + if !match.Matched { + continue + } + if match.Entry == nil { + return match + } + if resolved.Entry != nil && resolved.Entry.ModelContextCapacity != match.Entry.ModelContextCapacity { + return extensionv1.CatalogMatch{Matched: true} + } + resolved = match + } + return resolved +} diff --git a/plugins/model-policy/catalog/catalog_data.go b/backend/internal/service/model_context_official_catalog_data.go similarity index 99% rename from plugins/model-policy/catalog/catalog_data.go rename to backend/internal/service/model_context_official_catalog_data.go index 9ac27f8072e7..d51a2c5e0070 100644 --- a/plugins/model-policy/catalog/catalog_data.go +++ b/backend/internal/service/model_context_official_catalog_data.go @@ -1,18 +1,4 @@ -package catalog - -import extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" - -const ModelContextCapacityBasisMaximum = extensionv1.ModelContextCapacityBasisMaximum -const ModelContextCapacityBasisTotal = extensionv1.ModelContextCapacityBasisTotal - -type ModelContextCapacity = extensionv1.ModelContextCapacity -type ModelContextCapacityReference = extensionv1.ModelContextCapacityReference -type OfficialModelContextCapacity = extensionv1.OfficialModelContextCapacity - -const ( - AccountModeCoding = "coding" - AccountModePayG = "payg" -) +package service // This release-pinned catalog describes capacity, not model availability or routing. // Stored IDs/aliases are exact; matching-only namespace and GPT spelling variants diff --git a/backend/internal/service/model_context_official_catalog_test.go b/backend/internal/service/model_context_official_catalog_test.go new file mode 100644 index 000000000000..24766456bb04 --- /dev/null +++ b/backend/internal/service/model_context_official_catalog_test.go @@ -0,0 +1,77 @@ +package service + +import ( + "testing" + + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +func TestOfficialModelCatalogPreservesProductScope(t *testing.T) { + for _, tc := range []struct { + name, model, host string + want bool + }{ + {"known model", "gpt-6-astra", "api.openai.com", true}, + {"unknown date suffix", "gpt-6-astra-2099-01-01", "api.openai.com", false}, + {"known Bailian endpoint", "qwen3.8-max", "dashscope.aliyuncs.com", true}, + {"unrelated endpoint", "qwen3.8-max", "relay.example.test", false}, + } { + t.Run(tc.name, func(t *testing.T) { + query := extensionv1.CatalogQuery{Candidates: []string{tc.model}, Platform: "openai", AccountType: "apikey", Scheme: "https", Host: tc.host} + match, err := resolveOfficialModelCatalog(query) + if err != nil || (match.Entry != nil) != tc.want { + t.Fatalf("unexpected reference: %+v, %v", match, err) + } + }) + } +} + +func TestOfficialModelCatalogSnapshotCannotMutateActiveReferences(t *testing.T) { + before := OfficialModelCatalogSnapshot() + after := OfficialModelCatalogSnapshot() + after[0].Aliases[0] = "not-a-real-alias" + if OfficialModelCatalogSnapshot()[0].Aliases[0] != before[0].Aliases[0] { + t.Fatal("catalog exposed mutable alias storage") + } +} + +func TestOfficialModelCatalogGPT6SeparatesAPIAndCodex(t *testing.T) { + for _, model := range []string{"gpt-6-sol", "gpt-6-luna"} { + query := extensionv1.CatalogQuery{Candidates: []string{model}, Platform: "openai", AccountType: "apikey", Scheme: "https", Host: "api.openai.com"} + match, err := resolveOfficialModelCatalog(query) + if err != nil || match.Entry == nil { + t.Fatalf("%s: reference unavailable: %v", model, err) + } + entry := match.Entry + if entry.Product != "api" || entry.ContextWindow != 1050000 || entry.MaxOutputTokens != 128000 || entry.MaxInputTokens != 0 || len(entry.Aliases) != 0 { + t.Fatalf("%s: incorrect API capacity or invented alias: %+v", model, entry) + } + ref := entry.Reference + if ref == nil || ref.ContextWindow != 272000 || ref.MaxContextWindow != 872000 || ref.SourceURL != GPT6ContextCapacityReferenceSource || ref.Release != GPT6ContextCapacityReferenceCommit { + t.Fatalf("%s: incorrect subscription reference: %+v", model, ref) + } + query.Candidates = []string{model + "-2099-01-01"} + unknown, err := resolveOfficialModelCatalog(query) + if err != nil || unknown.Entry != nil { + t.Fatal("unpublished dated snapshots must not inherit capacity") + } + } +} + +func TestMergeCatalogMatchesKeepsPluginAmbiguityRule(t *testing.T) { + a := &OfficialModelContextCapacity{ModelID: "a", ModelContextCapacity: ModelContextCapacity{ContextWindow: 100}} + b := &OfficialModelContextCapacity{ModelID: "b", ModelContextCapacity: ModelContextCapacity{ContextWindow: 200}} + same := &OfficialModelContextCapacity{ModelID: "same", ModelContextCapacity: ModelContextCapacity{ContextWindow: 100}} + if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true, Entry: a}, extensionv1.CatalogMatch{}); got.Entry != a { + t.Fatalf("a single match must win: %+v", got) + } + if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true, Entry: a}, extensionv1.CatalogMatch{Matched: true, Entry: b}); !got.Matched || got.Entry != nil { + t.Fatalf("different capacities must be ambiguous: %+v", got) + } + if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true, Entry: a}, extensionv1.CatalogMatch{Matched: true, Entry: same}); got.Entry != same { + t.Fatalf("the later equal-capacity entry must win: %+v", got) + } + if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true}, extensionv1.CatalogMatch{Matched: true, Entry: a}); !got.Matched || got.Entry != nil { + t.Fatalf("an ambiguous answer must stay ambiguous: %+v", got) + } +} diff --git a/backend/internal/service/plugin_catalog_test.go b/backend/internal/service/plugin_catalog_test.go index 4240358fdfea..12ebfba1a142 100644 --- a/backend/internal/service/plugin_catalog_test.go +++ b/backend/internal/service/plugin_catalog_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - modelcatalog "github.com/HTExplicit/sub2api-plugins/modelpolicy/catalog" "github.com/Wei-Shaw/sub2api/internal/config" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/stretchr/testify/require" @@ -222,8 +221,8 @@ func TestPluginCatalogDiscardsReplyFromChangedConfigRevision(t *testing.T) { func TestPluginCatalogNamespaceLimitMatchesHostCandidates(t *testing.T) { previous := processExtensionCatalog.Load() t.Cleanup(func() { processExtensionCatalog.Store(previous) }) - module := modelcatalog.New() - processExtensionCatalog.Store(&extensionCatalogProvider{resolver: module}) + // The official catalog resolves in process; no plugin catalog is installed. + processExtensionCatalog.Store(nil) account := &Account{ID: 42, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"base_url": "https://api.openai.com/v1"}} model := strings.Repeat("namespace", 35) + "/gpt-6-astra" require.Greater(t, len(model), 300) @@ -239,6 +238,6 @@ func TestPluginCatalogNamespaceLimitMatchesHostCandidates(t *testing.T) { require.Len(t, tooLong, 513) require.False(t, validModelContextID(tooLong)) require.Nil(t, LookupOfficialModelContextCapacity(account, tooLong)) - _, err := module.ResolveCatalog(context.Background(), extensionv1.CatalogQuery{Candidates: modelContextReferenceCandidates(tooLong), Platform: PlatformOpenAI, AccountType: AccountTypeAPIKey}) - require.Error(t, err, "the plugin must independently reject a 513-byte candidate") + _, err := resolveOfficialModelCatalog(extensionv1.CatalogQuery{Candidates: modelContextReferenceCandidates(tooLong), Platform: PlatformOpenAI, AccountType: AccountTypeAPIKey}) + require.Error(t, err, "the official catalog must independently reject a 513-byte candidate") } diff --git a/backend/internal/service/plugin_extension_runtime_test.go b/backend/internal/service/plugin_extension_runtime_test.go index c61110d57cbd..4a9f083bf6a9 100644 --- a/backend/internal/service/plugin_extension_runtime_test.go +++ b/backend/internal/service/plugin_extension_runtime_test.go @@ -68,33 +68,6 @@ func TestExtensionRuntimeUsesOfficialProcessAndHostBroker(t *testing.T) { // HTTP upstream exists in this test, so it cannot issue a real model request. } -func TestCatalogExtensionRuntimeUsesIndependentProcess(t *testing.T) { - binary := os.Getenv("SUB2API_MODEL_POLICY_TEST_BINARY") - if binary == "" { - t.Skip("set SUB2API_MODEL_POLICY_TEST_BINARY to the independent model-policy program") - } - data, err := os.ReadFile(binary) - require.NoError(t, err) - digest := sha256.Sum256(data) - installation := &PluginInstallation{ID: 8, PluginKey: "codexrip.model-policy", Version: firstPartyTestVersion(t, "model-policy"), BinaryPath: binary, BinarySHA256: hex.EncodeToString(digest[:]), Manifest: PluginManifest{Requires: PluginRequirements{ExtensionAPI: 1}, Capabilities: []PluginCapability{{ID: extensionv1.CapabilityCatalog, Platform: "*", AccountType: "*"}}}} - host := newPluginHostServiceServer(installation.PluginKey, nil, nil, PluginAccountScope{}) - host.extension = &pluginExtensionHost{key: installation.PluginKey, state: &extensionReadOnlyFixture{}} - socketDir := filepath.Join(t.TempDir(), "runtime") - require.NoError(t, os.MkdirAll(socketDir, 0700)) - runtime, err := startPluginRuntime(context.Background(), installation, 15*time.Second, socketDir, host) - require.NoError(t, err) - t.Cleanup(runtime.kill) - require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{}`))) - raw, _ := json.Marshal(extensionv1.CatalogQuery{Candidates: []string{"gpt-6-astra"}, Platform: PlatformOpenAI, AccountType: AccountTypeAPIKey, Scheme: "https", Host: "api.openai.com"}) - result, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityCatalog, Operation: "resolve", Payload: raw}) - require.NoError(t, err) - var match extensionv1.CatalogMatch - require.NoError(t, json.Unmarshal(result.Payload, &match)) - require.NotNil(t, match.Entry) - require.EqualValues(t, 1050000, match.Entry.ContextWindow) - require.Equal(t, "gpt-6-astra", match.Entry.ModelID) -} - func TestPromptExtensionRuntimeUsesIndependentProcess(t *testing.T) { binary := os.Getenv("SUB2API_PROMPT_SKILLS_TEST_BINARY") if binary == "" { diff --git a/backend/internal/testextensions/fixtures.go b/backend/internal/testextensions/fixtures.go index ac3d5e18fb35..6c2bf5bd68bd 100644 --- a/backend/internal/testextensions/fixtures.go +++ b/backend/internal/testextensions/fixtures.go @@ -15,7 +15,6 @@ import ( accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" observability "github.com/HTExplicit/sub2api-plugins/adminobservability/policy" imagetools "github.com/HTExplicit/sub2api-plugins/imagetools/policy" - catalog "github.com/HTExplicit/sub2api-plugins/modelpolicy/catalog" prompt "github.com/HTExplicit/sub2api-plugins/promptskills/policy" "github.com/Wei-Shaw/sub2api/internal/service" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" @@ -71,8 +70,8 @@ func (fixture operations) InvokeOperation(ctx context.Context, _, _ string, in e } return extensionv1.Result{}, service.ErrExtensionOperationDisabled } -func Install() { service.ConfigureProcessExtensionServices(catalog.New(), operations{}) } +func Install() { service.ConfigureProcessExtensionServices(nil, operations{}) } func InstallImageTools(config extensionv1.ImageToolsConfig) { - service.ConfigureProcessExtensionServices(catalog.New(), operations{imageConfig: &config}) + service.ConfigureProcessExtensionServices(nil, operations{imageConfig: &config}) } diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 03d28030b20d..ac12862200b7 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -1119,6 +1119,41 @@ export interface UpdateSettingsRequest { allow_user_view_error_requests?: boolean; } +export interface OfficialModelCapacityReference { + product: string + source_url: string + release: string + verified_at: string + context_window: number + max_context_window: number +} + +export interface OfficialModelCapacityEntry { + model_id: string + aliases?: string[] + provider: string + product: string + context_window?: number + max_context_window?: number + max_input_tokens?: number + max_output_tokens?: number + source_url: string + verified_at: string + conditions?: string + reference?: OfficialModelCapacityReference +} + +export interface OfficialModelCapacityCatalog { + reference_release: string + entries: OfficialModelCapacityEntry[] +} + +/** Read-only release-pinned official model capacity catalog. */ +export async function getOfficialModelCapacityCatalog(): Promise { + const { data } = await apiClient.get("/admin/settings/model-context-catalog"); + return data; +} + /** * Get all system settings * @returns System settings @@ -1635,6 +1670,7 @@ export async function resetWebSearchUsage(payload: { export const settingsAPI = { getSettings, + getOfficialModelCapacityCatalog, updateSettings, testSmtpConnection, sendTestEmail, diff --git a/frontend/src/components/admin/OfficialModelCatalogPanel.vue b/frontend/src/components/admin/OfficialModelCatalogPanel.vue new file mode 100644 index 000000000000..b68c0849df94 --- /dev/null +++ b/frontend/src/components/admin/OfficialModelCatalogPanel.vue @@ -0,0 +1,82 @@ + + + diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts index 1ba4fd66752d..31a7af3e9f51 100644 --- a/frontend/src/i18n/locales/en/admin/settings.ts +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -523,6 +523,17 @@ export default { saved: 'OpenCode Go usage refresh settings saved', saveFailed: 'Failed to save OpenCode Go usage refresh settings' }, + officialModelCatalog: { + title: 'Model catalog and capacity references', + description: 'References support capacity planning. Account overrides and live upstream declarations retain their existing precedence.', + search: 'Search model or provider', + count: '{count} matches; showing the first {shown}.', + context: 'Context', + maxOutput: 'Max output', + subscriptionMaximum: 'Subscription reference maximum', + verified: 'Verified', + loadFailed: 'Failed to load the model catalog' + }, gatewayForwarding: { title: 'Request Forwarding', description: 'Control how requests are forwarded to upstream OAuth accounts', diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts index f929df7c90f5..5fe32804064a 100644 --- a/frontend/src/i18n/locales/zh/admin/settings.ts +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -516,6 +516,17 @@ export default { saved: 'OpenCode Go 用量刷新设置已保存', saveFailed: '保存 OpenCode Go 用量刷新设置失败' }, + officialModelCatalog: { + title: '模型目录与容量参考', + description: '参考数据用于容量规划;账号自定义值与实时上游声明继续按既有优先级处理。', + search: '搜索模型或供应商', + count: '{count} 项匹配,显示前 {shown} 项。', + context: '上下文', + maxOutput: '最大输出', + subscriptionMaximum: '订阅参考最大窗口', + verified: '核对日期', + loadFailed: '模型目录加载失败' + }, gatewayForwarding: { title: '请求转发行为', description: '控制请求转发到上游 OAuth 账号时的行为', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index a490b2d21056..3122733e0cbe 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -4503,6 +4503,7 @@
+

{{ t("admin.settings.gatewayForwarding.codexClientRestrictionTitle") }} @@ -9166,6 +9167,7 @@ -

From 254fb779fad4e2ad47f6a4a5e69abf5b044f252b Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 10:03:21 +0800 Subject: [PATCH 02/13] refactor(deplugin): fold image-tools into core image policy and native pages Image request policy (native Cindy image validation, Responses image bridge planning, Image Studio request plans and model choices) now runs in process instead of the image-tools plugin. The Image Studio and bridge switches live in the image_tools_config setting, fall back to the deploy-time rollout flags, load at startup, and are edited through a native settings panel (GET/PUT /api/v1/admin/settings/image-tools). Switching Image Studio off still cancels running executions, as the plugin policy lease did. The Image Studio page becomes a native view: the plugin Vue page moves to frontend/src/features/image-studio and calls the host routes through a native resource() with the same signature. The routes lose their plugin resource admission, the sidebar and route guard follow the public image_studio_enabled flag, and the plugin module, bundle entry, go.mod replace and process contract test are removed. Plugin-boundary privacy tests are dropped; policy tests move to the service package. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/test_first_party_plugins.sh | 3 +- .github/workflows/downstream-verify.yml | 4 +- backend/go.mod | 3 - .../internal/handler/admin/setting_handler.go | 22 ++ backend/internal/server/routes/admin.go | 2 + backend/internal/server/routes/user.go | 29 +-- .../cindy_native_image_snapshot_test.go | 20 +- .../cindy_native_messages_scope_test.go | 3 + .../service/cindy_pricing_snapshot_test.go | 3 + .../service/image_studio_job_service.go | 13 +- .../internal/service/image_studio_runtime.go | 9 +- .../service/image_studio_runtime_test.go | 24 +- .../{plugin_image_tools.go => image_tools.go} | 110 ++++---- .../internal/service/image_tools_policy.go | 237 ++++++++++++++++++ .../service/image_tools_policy_test.go | 126 ++++++++++ .../service/openai_gateway_forward.go | 8 - backend/internal/service/openai_images.go | 9 - .../service/plugin_extension_runtime_test.go | 38 --- .../service/plugin_feature_decode_test.go | 19 -- .../internal/service/plugin_image_bridge.go | 28 +-- .../service/plugin_image_bridge_test.go | 39 +-- .../service/plugin_prompt_fixture_test.go | 9 - .../internal/service/setting_image_tools.go | 66 +++++ backend/internal/service/wire.go | 2 + backend/internal/testextensions/fixtures.go | 25 +- frontend/src/api/admin/settings.ts | 18 ++ .../admin/ImageToolsSettingsPanel.vue | 61 +++++ frontend/src/components/layout/AppSidebar.vue | 2 +- .../features/image-studio/ImageStudioPage.vue | 18 +- .../__tests__/ImageStudioPage.spec.ts | 17 +- .../image-studio}/__tests__/api.spec.ts | 2 +- .../src/features/image-studio}/api.ts | 2 +- .../src/features/image-studio}/locales/en.ts | 0 .../src/features/image-studio}/locales/zh.ts | 0 .../src/i18n/locales/en/admin/settings.ts | 10 + .../src/i18n/locales/zh/admin/settings.ts | 10 + .../router/__tests__/feature-access.spec.ts | 12 +- frontend/src/router/index.ts | 11 +- frontend/src/utils/browserActions.ts | 17 ++ frontend/src/utils/nativeResource.ts | 78 ++++++ frontend/src/views/admin/SettingsView.vue | 2 + frontend/src/views/user/ImageStudioView.vue | 5 +- plugins/bundle.source.json | 1 - plugins/image-tools/cmd/plugin/main.go | 12 - plugins/image-tools/go.mod | 24 -- plugins/image-tools/go.sum | 83 ------ plugins/image-tools/manifest.source.json | 36 --- plugins/image-tools/policy/bridge.go | 122 --------- plugins/image-tools/policy/bridge_test.go | 66 ----- plugins/image-tools/policy/module.go | 182 -------------- plugins/image-tools/policy/module_test.go | 43 ---- plugins/image-tools/policy/native.go | 50 ---- plugins/image-tools/policy/native_test.go | 40 --- plugins/image-tools/scripts/check-ui.py | 136 ---------- plugins/image-tools/ui/assets/app.js | 33 --- plugins/image-tools/ui/assets/styles.css | 11 - plugins/image-tools/ui/index.html | 3 - plugins/image-tools/ui/src/history.ts | 10 - plugins/image-tools/ui/src/index.html | 2 - plugins/image-tools/ui/src/main.ts | 6 - 60 files changed, 797 insertions(+), 1179 deletions(-) rename backend/internal/service/{plugin_image_tools.go => image_tools.go} (55%) create mode 100644 backend/internal/service/image_tools_policy.go create mode 100644 backend/internal/service/image_tools_policy_test.go create mode 100644 backend/internal/service/setting_image_tools.go create mode 100644 frontend/src/components/admin/ImageToolsSettingsPanel.vue rename plugins/image-tools/ui/src/App.vue => frontend/src/features/image-studio/ImageStudioPage.vue (98%) rename plugins/image-tools/ui/src/__tests__/App.spec.ts => frontend/src/features/image-studio/__tests__/ImageStudioPage.spec.ts (96%) rename {plugins/image-tools/ui/src => frontend/src/features/image-studio}/__tests__/api.spec.ts (97%) rename {plugins/image-tools/ui/src => frontend/src/features/image-studio}/api.ts (99%) rename {plugins/image-tools/ui/src => frontend/src/features/image-studio}/locales/en.ts (100%) rename {plugins/image-tools/ui/src => frontend/src/features/image-studio}/locales/zh.ts (100%) create mode 100644 frontend/src/utils/browserActions.ts create mode 100644 frontend/src/utils/nativeResource.ts delete mode 100644 plugins/image-tools/cmd/plugin/main.go delete mode 100644 plugins/image-tools/go.mod delete mode 100644 plugins/image-tools/go.sum delete mode 100644 plugins/image-tools/manifest.source.json delete mode 100644 plugins/image-tools/policy/bridge.go delete mode 100644 plugins/image-tools/policy/bridge_test.go delete mode 100644 plugins/image-tools/policy/module.go delete mode 100644 plugins/image-tools/policy/module_test.go delete mode 100644 plugins/image-tools/policy/native.go delete mode 100644 plugins/image-tools/policy/native_test.go delete mode 100644 plugins/image-tools/scripts/check-ui.py delete mode 100644 plugins/image-tools/ui/assets/app.js delete mode 100644 plugins/image-tools/ui/assets/styles.css delete mode 100644 plugins/image-tools/ui/index.html delete mode 100644 plugins/image-tools/ui/src/history.ts delete mode 100644 plugins/image-tools/ui/src/index.html delete mode 100644 plugins/image-tools/ui/src/main.ts diff --git a/.github/scripts/test_first_party_plugins.sh b/.github/scripts/test_first_party_plugins.sh index 81f0c2adc59d..dc50934ad1c2 100644 --- a/.github/scripts/test_first_party_plugins.sh +++ b/.github/scripts/test_first_party_plugins.sh @@ -32,8 +32,7 @@ export SUB2API_EXTENSION_TEST_BUNDLE="$plugin_build_dir/bundle/lock.json" export SUB2API_PROMPT_SKILLS_TEST_BINARY="$plugin_build_dir/binaries/prompt-skills" export SUB2API_ACCOUNT_TOOLS_TEST_BINARY="$plugin_build_dir/binaries/account-tools" export SUB2API_CINDY_PROVIDER_TEST_BINARY="$plugin_build_dir/binaries/cindy-provider" -export SUB2API_IMAGE_TOOLS_TEST_BINARY="$plugin_build_dir/binaries/image-tools" export SUB2API_ADMIN_OBSERVABILITY_TEST_BINARY="$plugin_build_dir/binaries/admin-observability" export SUB2API_EXTENSION_TEST_PUBLIC_KEY SUB2API_EXTENSION_TEST_PUBLIC_KEY=$(jq -r .public_key "$plugin_build_dir/publisher.json") -go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestImageToolsExtensionRuntimeUsesIndependentProcess|TestObservabilityExtensionRuntimeUsesScopedMetricsBroker|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 +go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestObservabilityExtensionRuntimeUsesScopedMetricsBroker|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 diff --git a/.github/workflows/downstream-verify.yml b/.github/workflows/downstream-verify.yml index e9fd4b3c5dcb..c0ac4118637e 100644 --- a/.github/workflows/downstream-verify.yml +++ b/.github/workflows/downstream-verify.yml @@ -214,7 +214,7 @@ jobs: ../plugins/prompt-skills/ui/src/__tests__/locales.spec.ts \ src/views/admin/__tests__/AccountsView.console.spec.ts \ src/views/admin/__tests__/AccountsView.priorityColumn.spec.ts \ - ../plugins/image-tools/ui/src/__tests__/api.spec.ts \ + src/features/image-studio/__tests__/api.spec.ts \ src/api/__tests__/cindyBalanceProbe.spec.ts \ src/api/__tests__/admin.groups.cindy-split.spec.ts \ ../plugins/cindy-provider/ui/src/__tests__/CindyGroupAuditDialog.spec.ts \ @@ -227,7 +227,7 @@ jobs: src/components/layout/__tests__/AppSidebar.spec.ts \ src/components/layout/__tests__/AppSidebar.simple-mode.spec.ts \ src/views/admin/__tests__/GroupsView.columnSettings.spec.ts \ - ../plugins/image-tools/ui/src/__tests__/App.spec.ts \ + src/features/image-studio/__tests__/ImageStudioPage.spec.ts \ src/views/admin/__tests__/AccountsView.bulkEdit.spec.ts \ src/views/admin/__tests__/AccountsView.schedulerScore.spec.ts \ src/views/admin/__tests__/AccountsView.sparkShadow.spec.ts \ diff --git a/backend/go.mod b/backend/go.mod index 56a7334e627b..165c18228974 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,8 +10,6 @@ replace github.com/HTExplicit/sub2api-plugins/accounttools => ../plugins/account replace github.com/HTExplicit/sub2api-plugins/cindyprovider => ../plugins/cindy-provider -replace github.com/HTExplicit/sub2api-plugins/imagetools => ../plugins/image-tools - replace github.com/HTExplicit/sub2api-plugins/adminobservability => ../plugins/admin-observability require ( @@ -19,7 +17,6 @@ require ( github.com/HTExplicit/sub2api-plugins/promptskills v0.0.0 github.com/HTExplicit/sub2api-plugins/accounttools v0.0.0 github.com/HTExplicit/sub2api-plugins/cindyprovider v0.0.0 - github.com/HTExplicit/sub2api-plugins/imagetools v0.0.0 github.com/HTExplicit/sub2api-plugins/adminobservability v0.0.0 entgo.io/ent v0.14.5 github.com/DATA-DOG/go-sqlmock v1.5.2 diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index ce69e801d026..dc4c0e0c7cda 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -12,6 +12,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/handler/dto" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/gin-gonic/gin" ) @@ -111,6 +112,27 @@ func (h *SettingHandler) GetOfficialModelContextCatalog(c *gin.Context) { }) } +// GetImageToolsSettings returns the Image Studio and Responses image bridge switches. +// GET /api/v1/admin/settings/image-tools +func (h *SettingHandler) GetImageToolsSettings(c *gin.Context) { + response.Success(c, h.settingService.GetImageToolsConfig(c.Request.Context())) +} + +// UpdateImageToolsSettings saves the Image Studio and Responses image bridge switches. +// PUT /api/v1/admin/settings/image-tools +func (h *SettingHandler) UpdateImageToolsSettings(c *gin.Context) { + var req extensionv1.ImageToolsConfig + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid image tools settings") + return + } + if err := h.settingService.UpdateImageToolsConfig(c.Request.Context(), req); err != nil { + response.ErrorFrom(c, err) + return + } + response.Success(c, req) +} + func (h *SettingHandler) GetSettings(c *gin.Context) { c.Header("Cache-Control", "no-store") settings, err := h.settingService.GetAllSettings(c.Request.Context()) diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index e06081b4e06e..8dab25f47dd9 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -723,6 +723,8 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { adminSettings.GET("", h.Admin.Setting.GetSettings) adminSettings.PUT("", h.Admin.Setting.UpdateSettings) adminSettings.GET("/model-context-catalog", h.Admin.Setting.GetOfficialModelContextCatalog) + adminSettings.GET("/image-tools", h.Admin.Setting.GetImageToolsSettings) + adminSettings.PUT("/image-tools", h.Admin.Setting.UpdateImageToolsSettings) adminSettings.POST("/openai-codex-ticket/proxy-test", h.Admin.Setting.TestCodexTicketProxy) adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection) adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) diff --git a/backend/internal/server/routes/user.go b/backend/internal/server/routes/user.go index 2aacc55c3169..ff13eb2872dc 100644 --- a/backend/internal/server/routes/user.go +++ b/backend/internal/server/routes/user.go @@ -4,7 +4,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/handler" "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/gin-gonic/gin" ) @@ -99,25 +98,15 @@ func RegisterUserRoutes( } imageStudio := authenticated.Group("/image-studio") - for _, route := range []struct { - name, method, path, response string - retained bool - handler gin.HandlerFunc - }{ - {"image.keys", "GET", "/eligible-keys", "", false, h.ImageStudio.EligibleKeys}, - {"image.create", "POST", "/jobs", "", false, h.ImageStudio.Create}, - {"image.jobs", "GET", "/jobs", "", true, h.ImageStudio.List}, - {"image.job", "GET", "/jobs/:id", "", true, h.ImageStudio.Get}, - {"image.items", "GET", "/jobs/:id/items", "", true, h.ImageStudio.Items}, - {"image.cancel", "POST", "/jobs/:id/cancel", "", true, h.ImageStudio.Cancel}, - {"image.retry", "POST", "/jobs/:id/retry", "", false, h.ImageStudio.Retry}, - {"image.artifact", "GET", "/jobs/:id/artifacts/:artifact_id", "blob", true, h.ImageStudio.Artifact}, - } { - descriptor := extensionv1.ResourceDescriptor{ResourceGrant: extensionv1.ResourceGrant{Name: route.name, Capability: extensionv1.CapabilityRequest, Permission: "user"}, Method: route.method, Path: imageStudio.BasePath() + route.path, ResponseKind: route.response, Retained: route.retained} - imageStudio.Handle(route.method, route.path, h.Admin.Plugin.RegisterResource(descriptor), route.handler) - } - for _, name := range []string{"image.history.list", "image.history.save", "image.history.delete", "image.history.clear"} { - h.Admin.Plugin.RegisterResource(extensionv1.ResourceDescriptor{ResourceGrant: extensionv1.ResourceGrant{Name: name, Capability: extensionv1.CapabilityRequest, Permission: "user"}, Method: "LOCAL", Retained: true}) + { + imageStudio.GET("/eligible-keys", h.ImageStudio.EligibleKeys) + imageStudio.POST("/jobs", h.ImageStudio.Create) + imageStudio.GET("/jobs", h.ImageStudio.List) + imageStudio.GET("/jobs/:id", h.ImageStudio.Get) + imageStudio.GET("/jobs/:id/items", h.ImageStudio.Items) + imageStudio.POST("/jobs/:id/cancel", h.ImageStudio.Cancel) + imageStudio.POST("/jobs/:id/retry", h.ImageStudio.Retry) + imageStudio.GET("/jobs/:id/artifacts/:artifact_id", h.ImageStudio.Artifact) } // 使用记录(聚合统计属重查询,叠加更严格的按用户限流) diff --git a/backend/internal/service/cindy_native_image_snapshot_test.go b/backend/internal/service/cindy_native_image_snapshot_test.go index 3fb7ab024c13..e637e285574a 100644 --- a/backend/internal/service/cindy_native_image_snapshot_test.go +++ b/backend/internal/service/cindy_native_image_snapshot_test.go @@ -26,17 +26,6 @@ type cindyNativeImageSnapshotFixture struct { func (f *cindyNativeImageSnapshotFixture) InvokeOperation(ctx context.Context, platform, accountType string, in extensionv1.Invocation) (extensionv1.Result, error) { f.calls = append(f.calls, in) - if in.Operation == "image.features" { - raw, err := json.Marshal(f.before.Images) - return extensionv1.Result{Payload: raw}, err - } - if in.Operation == "image.native.validate" { - var facts extensionv1.ImageNativeRequest - if err := json.Unmarshal(in.Payload, &facts); err != nil { - return extensionv1.Result{}, err - } - f.nativeFacts = append(f.nativeFacts, facts) - } if in.Operation == "cindy.catalog" && !f.disabled { var query extensionv1.CindyCatalogQuery if err := json.Unmarshal(in.Payload, &query); err != nil { @@ -211,7 +200,14 @@ func newNativeImageSnapshotFixture(t *testing.T, studioEnabled bool) *cindyNativ before: before, after: after, pricing: pricing, pricingAfter: &pricingAfter, fallback: previous.invoker, }} processExtensionOperations.Store(&extensionOperationProvider{invoker: fixture}) - t.Cleanup(func() { processExtensionOperations.Store(previous) }) + images := before.Images + ConfigureImageTools(&images) + observeNativeImageFacts = func(facts extensionv1.ImageNativeRequest) { fixture.nativeFacts = append(fixture.nativeFacts, facts) } + t.Cleanup(func() { + processExtensionOperations.Store(previous) + ConfigureImageTools(nil) + observeNativeImageFacts = nil + }) return fixture } diff --git a/backend/internal/service/cindy_native_messages_scope_test.go b/backend/internal/service/cindy_native_messages_scope_test.go index d71dc5e583cd..4b7c52a7a0fd 100644 --- a/backend/internal/service/cindy_native_messages_scope_test.go +++ b/backend/internal/service/cindy_native_messages_scope_test.go @@ -19,6 +19,9 @@ import ( ) func TestCindyNativeMessagesUsesActualAccountAdmissionAndCapturedPricing(t *testing.T) { + // Image tool switches are host state now; keep them empty so only scope metadata is compared. + ConfigureImageTools(&extensionv1.ImageToolsConfig{}) + t.Cleanup(func() { ConfigureImageTools(nil) }) for _, tc := range []struct { name string accountID int64 diff --git a/backend/internal/service/cindy_pricing_snapshot_test.go b/backend/internal/service/cindy_pricing_snapshot_test.go index a14f77347e27..a3aefbbaca6a 100644 --- a/backend/internal/service/cindy_pricing_snapshot_test.go +++ b/backend/internal/service/cindy_pricing_snapshot_test.go @@ -96,6 +96,9 @@ func TestCindyNewTurnRefreshesPricingWithoutChangingPendingBill(t *testing.T) { } func TestCindyPricingScopeCannotReuseAnExcludedAccountPolicyCache(t *testing.T) { + // Image tool switches are host state now; keep them empty so only scope metadata is compared. + ConfigureImageTools(&extensionv1.ImageToolsConfig{}) + t.Cleanup(func() { ConfigureImageTools(nil) }) previous := processExtensionOperations.Load() t.Cleanup(func() { processExtensionOperations.Store(previous) }) module := cindy.New() diff --git a/backend/internal/service/image_studio_job_service.go b/backend/internal/service/image_studio_job_service.go index 7292a77cac35..bdd723d1185a 100644 --- a/backend/internal/service/image_studio_job_service.go +++ b/backend/internal/service/image_studio_job_service.go @@ -2,14 +2,11 @@ package service import ( "context" - "encoding/json" "errors" "fmt" "io" "strings" "time" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) type ImageStudioUpload struct { @@ -129,15 +126,7 @@ func imageStudioModelCapabilities() []CindyModelCapability { capabilities = append(capabilities, cindyModelCapabilityFromCapability(capability)) } } - raw, _ := json.Marshal(extensionv1.ImageStudioCatalogRequest{Capabilities: capabilities}) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - out, err := invokeProcessExtensionCached(ctx, "*", "*", extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.studio.models", Payload: raw}) - result := make([]CindyModelCapability, 0) - if err == nil && out.Code == "" { - _ = json.Unmarshal(out.Payload, &result) - } - return result + return imageStudioModelChoices(capabilities) } func (s *ImageStudioService) eligibleAPIKey(ctx context.Context, userID, apiKeyID int64) (*APIKey, error) { diff --git a/backend/internal/service/image_studio_runtime.go b/backend/internal/service/image_studio_runtime.go index 709f8fd1b101..97acac073871 100644 --- a/backend/internal/service/image_studio_runtime.go +++ b/backend/internal/service/image_studio_runtime.go @@ -6,8 +6,6 @@ import ( "fmt" "sync" "time" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) type ImageStudioRuntimeOptions struct { @@ -155,13 +153,8 @@ func (r *ImageStudioRuntime) processClaim(ctx context.Context, claim *ImageStudi return } job := claim.Job - policyCtx, release, err := bindProcessExtensionContext(ctx, "*", "*", extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.studio.plan"}) - if err != nil { - r.failClaim(ctx, job.ID, claim.Item.ID, newImageStudioError(503, "studio_unavailable", "Image Studio is unavailable")) - return - } + ctx, release := bindImageStudioEnabled(ctx) defer release() - ctx = policyCtx if err := EnsureImageStudioAvailable(ctx); err != nil { r.failClaim(ctx, job.ID, claim.Item.ID, err) return diff --git a/backend/internal/service/image_studio_runtime_test.go b/backend/internal/service/image_studio_runtime_test.go index 195627b19c78..80be75f9d8ca 100644 --- a/backend/internal/service/image_studio_runtime_test.go +++ b/backend/internal/service/image_studio_runtime_test.go @@ -22,25 +22,9 @@ type imageStudioRuntimeRepoFake struct { failed atomic.Int32 } -type imageStudioBoundPolicy struct { - promptPolicyFixture - runtime *pluginRuntime -} - -func (p imageStudioBoundPolicy) BindOperationContext(ctx context.Context, _, _ string, _ extensionv1.Invocation) (context.Context, context.CancelFunc, error) { - return p.runtime.bindPolicyContext(ctx) -} - func TestImageStudioDisableCancelsHostExecutionAndKeepsSavedInputs(t *testing.T) { - previousFlag := cindyRolloutFeatures.imageStudio - cindyRolloutFeatures.imageStudio = true - previousProvider := processExtensionOperations.Load() - t.Cleanup(func() { - cindyRolloutFeatures.imageStudio = previousFlag - processExtensionOperations.Store(previousProvider) - }) - plugin := &pluginRuntime{} - processExtensionOperations.Store(&extensionOperationProvider{invoker: imageStudioBoundPolicy{runtime: plugin}}) + ConfigureImageTools(&extensionv1.ImageToolsConfig{StudioEnabled: true}) + t.Cleanup(func() { ConfigureImageTools(nil) }) group, key, account := canonicalImageStudioFixture() repo := &imageStudioRuntimeRepoFake{} store := &imageStudioStoreFake{} @@ -55,11 +39,11 @@ func TestImageStudioDisableCancelsHostExecutionAndKeepsSavedInputs(t *testing.T) case <-time.After(time.Second): t.Fatal("execution did not start") } - plugin.beginDrain() + ConfigureImageTools(&extensionv1.ImageToolsConfig{}) select { case <-done: case <-time.After(time.Second): - t.Fatal("plugin disable did not cancel execution") + t.Fatal("disabling Image Studio did not cancel execution") } require.EqualValues(t, 1, repo.failed.Load()) require.Zero(t, repo.completed.Load()) diff --git a/backend/internal/service/plugin_image_tools.go b/backend/internal/service/image_tools.go similarity index 55% rename from backend/internal/service/plugin_image_tools.go rename to backend/internal/service/image_tools.go index 95f962bc895a..3e0dd51dc4e1 100644 --- a/backend/internal/service/plugin_image_tools.go +++ b/backend/internal/service/image_tools.go @@ -2,26 +2,67 @@ package service import ( "context" - "encoding/json" "errors" "slices" "strings" - "time" + "sync" + "sync/atomic" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) +var imageToolsConfigOverride atomic.Pointer[extensionv1.ImageToolsConfig] + +// currentImageToolsConfig returns the effective Image Studio and Responses image +// bridge switches: the stored administrator setting once loaded, otherwise the +// deploy-time rollout flags (the value the former image-tools plugin was seeded with). func currentImageToolsConfig() (extensionv1.ImageToolsConfig, bool) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - result, err := invokeProcessExtensionCached(ctx, "*", "*", extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.features", Payload: json.RawMessage(`{}`)}) - var config extensionv1.ImageToolsConfig - if err != nil || result.Code != "" || json.Unmarshal(result.Payload, &config) != nil { - return extensionv1.ImageToolsConfig{}, false + if config := imageToolsConfigOverride.Load(); config != nil { + return *config, true + } + return LegacyImageToolsConfig(), true +} + +// imageStudioStop is closed whenever Image Studio is switched off so running +// executions stop, as the former plugin's policy lease did. +var imageStudioStop = struct { + mu sync.Mutex + ch chan struct{} +}{ch: make(chan struct{})} + +// ConfigureImageTools installs the effective switches (startup load, admin +// update, tests). A nil value falls back to the deploy-time rollout flags. +func ConfigureImageTools(config *extensionv1.ImageToolsConfig) { + imageToolsConfigOverride.Store(config) + if current, _ := currentImageToolsConfig(); !current.StudioEnabled { + imageStudioStop.mu.Lock() + close(imageStudioStop.ch) + imageStudioStop.ch = make(chan struct{}) + imageStudioStop.mu.Unlock() } - return config, true } +// bindImageStudioEnabled returns a context that is canceled when Image Studio +// is switched off. +func bindImageStudioEnabled(ctx context.Context) (context.Context, context.CancelFunc) { + imageStudioStop.mu.Lock() + stop := imageStudioStop.ch + imageStudioStop.mu.Unlock() + ctx, cancel := context.WithCancel(ctx) + go func() { + select { + case <-stop: + cancel() + case <-ctx.Done(): + } + }() + return ctx, cancel +} + +// observeNativeImageFacts lets tests inspect the facts the host derives for the +// native image policy. It is nil in production. +var observeNativeImageFacts func(extensionv1.ImageNativeRequest) + func ValidateCindyImageRequest(model string, request *OpenAIImagesRequest) error { return ValidateCindyImageRequestForAccount(context.Background(), nil, model, request) } @@ -47,36 +88,17 @@ func ValidateCindyImageRequestForAccount(ctx context.Context, account *Account, verified := found && capability.PublicModel && snapshot.Config.CatalogEnabled && snapshot.Images.StudioEnabled && slices.Contains(capability.VerifiedEndpoints, endpoint) facts := extensionv1.ImageNativeRequest{Model: strings.TrimSpace(model), Capability: capability, Verified: verified, Editing: request.IsEdits(), Stream: request.Stream, Count: request.N, Size: strings.TrimSpace(request.Size), Quality: strings.TrimSpace(request.Quality), ResponseFormat: strings.TrimSpace(request.ResponseFormat), HasReference: request.InputImageCount() > 0, HasMask: request.HasMask, UnverifiedControls: request.Background != "" || request.OutputFormat != "" || request.Moderation != "" || request.InputFidelity != "" || request.Style != "" || request.OutputCompression != nil || request.PartialImages != nil} - raw, err := json.Marshal(facts) - if err != nil { - return ErrExtensionOperationUnavailable + if observeNativeImageFacts != nil { + observeNativeImageFacts(facts) } - in := extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.native.validate", Payload: raw} - if account != nil { - in.AccountID = account.ID - } - call, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - result, err := invokeProcessExtension(call, PlatformCindy, AccountTypeAPIKey, in) - if err != nil { - return err - } - if result.Code != "" { - return errors.New(result.Message) - } - var valid bool - if json.Unmarshal(result.Payload, &valid) != nil || !valid { - return ErrExtensionOperationUnavailable - } - return nil + return validateNativeImageRequest(facts) } func EnsureImageStudioAvailable(ctx context.Context) error { - result, err := invokeProcessExtensionCached(ctx, "*", "*", extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.features", Payload: json.RawMessage(`{}`)}) - var config extensionv1.ImageToolsConfig - if err != nil || result.Code != "" || json.Unmarshal(result.Payload, &config) != nil { + if err := ctx.Err(); err != nil { return newImageStudioError(503, "studio_unavailable", "Image Studio is unavailable") } + config, _ := currentImageToolsConfig() if !config.StudioEnabled { return newImageStudioError(404, "studio_disabled", "Image Studio is not enabled") } @@ -85,23 +107,15 @@ func EnsureImageStudioAvailable(ctx context.Context) error { func planImageStudio(ctx context.Context, input ImageStudioCreateInput, hasReference, hasMask, execute bool) (extensionv1.ImageStudioPlan, error) { request := extensionv1.ImageStudioPlanRequest{APIKeySelected: input.APIKeyID > 0, Mode: string(input.Mode), Model: input.Model, PromptBytes: len(strings.TrimSpace(input.Prompt)), Count: input.Count, Size: input.Size, Quality: input.Quality, HasReference: hasReference, HasMask: hasMask} - raw, _ := json.Marshal(request) - operation := "image.studio.validate" - if execute { - operation = "image.studio.plan" - } - call, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - result, err := invokeProcessExtension(call, "*", "*", extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: operation, Payload: raw}) - var plan extensionv1.ImageStudioPlan - if err != nil { - return plan, newImageStudioError(503, "studio_unavailable", "Image Studio is unavailable") + if err := ctx.Err(); err != nil { + return extensionv1.ImageStudioPlan{}, newImageStudioError(503, "studio_unavailable", "Image Studio is unavailable") } - if result.Code != "" { - return plan, newImageStudioError(result.HTTPStatus, result.Code, result.Message) + plan, code, message := planImageStudioRequest(request) + if code != "" { + return plan, newImageStudioError(400, code, message) } - if json.Unmarshal(result.Payload, &plan) != nil || plan.Model == "" || plan.OutputPerRequest != 1 || (plan.Endpoint != "/v1/images/generations" && plan.Endpoint != "/v1/images/edits") || plan.ResponseFormat != "b64_json" { - return plan, newImageStudioError(503, "studio_unavailable", "Image Studio returned an invalid request plan") + if config, _ := currentImageToolsConfig(); execute && !config.StudioEnabled { + return plan, newImageStudioError(404, "studio_disabled", "Image Studio is not enabled") } return plan, nil } diff --git a/backend/internal/service/image_tools_policy.go b/backend/internal/service/image_tools_policy.go new file mode 100644 index 000000000000..e91f2f2e6c62 --- /dev/null +++ b/backend/internal/service/image_tools_policy.go @@ -0,0 +1,237 @@ +package service + +import ( + "fmt" + "math" + "slices" + "strings" + + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +// Image request policy formerly served by the image-tools plugin process. These +// are pure checks over host facts; the switches come from currentImageToolsConfig. + +func validateNativeImageRequest(in extensionv1.ImageNativeRequest) error { + capability := in.Capability + if !capability.PublicModel || capability.Kind != extensionv1.CindyModelKindImage || capability.Controls == nil { + return fmt.Errorf("model %q has no verified Cindy image capability", in.Model) + } + endpoint := extensionv1.CindyEndpointImagesGenerate + controls := capability.Controls.Generation + if in.Editing { + endpoint, controls = extensionv1.CindyEndpointImagesEdit, capability.Controls.Edit + } + if !in.Verified || controls == nil { + return fmt.Errorf("model %q is not verified for %s", capability.PublicID, endpoint) + } + if in.Stream { + return fmt.Errorf("stream is not verified for model %q on %s", capability.PublicID, endpoint) + } + if controls.MaxOutputCount <= 0 || in.Count <= 0 || in.Count > controls.MaxOutputCount { + return fmt.Errorf("n must be between 1 and %d for model %q on %s", controls.MaxOutputCount, capability.PublicID, endpoint) + } + if !imageControlAllows(controls.Sizes, in.Size) { + return fmt.Errorf("size %q is not verified for model %q on %s", in.Size, capability.PublicID, endpoint) + } + if !imageControlAllows(controls.Qualities, in.Quality) { + return fmt.Errorf("quality %q is not verified for model %q on %s", in.Quality, capability.PublicID, endpoint) + } + if in.ResponseFormat != "" && !strings.EqualFold(in.ResponseFormat, "b64_json") { + return fmt.Errorf("response_format must be b64_json for model %q", capability.PublicID) + } + if in.UnverifiedControls { + return fmt.Errorf("request contains an unverified image control for model %q on %s", capability.PublicID, endpoint) + } + if in.Editing { + if !controls.SupportsReferenceImage || !in.HasReference { + return fmt.Errorf("a reference image is required for model %q on %s", capability.PublicID, endpoint) + } + if in.HasMask && !controls.SupportsMask { + return fmt.Errorf("mask is not verified for model %q on %s", capability.PublicID, endpoint) + } + } + return nil +} + +func planResponsesImageBridge(in extensionv1.ImageBridgeRequest, config extensionv1.ImageToolsConfig) (extensionv1.ImageBridgePlan, string, string) { + plan := extensionv1.ImageBridgePlan{} + resolve := func(model string) (extensionv1.CindyCapability, bool) { + if target, ok := in.Aliases[model]; ok { + model = target + } + for _, capability := range in.Capabilities { + if model == capability.PublicID || model == capability.LiveUpstreamID { + return capability, true + } + } + return extensionv1.CindyCapability{}, false + } + available := func(c extensionv1.CindyCapability) bool { + return c.PublicModel && (in.CatalogEnabled || + (c.Kind == extensionv1.CindyModelKindImage && config.StudioEnabled) || + (c.PublicID == "gpt-image-2" && config.ResponsesImageEnabled)) + } + supports := func(c extensionv1.CindyCapability) bool { + return config.ResponsesImageEnabled && c.PublicModel && c.PublicID == "gpt-image-2" && + c.Kind == extensionv1.CindyModelKindImage && slices.Contains(c.VerifiedEndpoints, extensionv1.CindyEndpointImagesGenerate) + } + top, known := resolve(in.Model) + plan.Supported = known && supports(top) + if in.Stage == "supports" { + return plan, "", "" + } + if in.Stage == "map" { + if len(in.Tools) > 0 && !config.ResponsesImageEnabled { + return plan, "image_model_not_found", "Responses image bridge is disabled" + } + if known && available(top) { + plan.Model = top.LiveUpstreamID + } + for _, tool := range in.Tools { + if capability, ok := resolve(tool.Model); ok && available(capability) && capability.Kind == extensionv1.CindyModelKindImage { + plan.Tools = append(plan.Tools, extensionv1.ImageBridgeModelPlan{Index: tool.Index, Model: capability.LiveUpstreamID}) + } + } + return plan, "", "" + } + if in.Stage != "validate" { + return plan, "invalid_bridge_stage", "Invalid image bridge stage" + } + modelError := func(model string) (extensionv1.ImageBridgePlan, string, string) { + return plan, "image_model_not_found", fmt.Sprintf("cindy Responses image tool model is not verified: %q", model) + } + if known && top.Kind == extensionv1.CindyModelKindImage && !plan.Supported { + return modelError(in.Model) + } + if plan.Supported { + if top.Controls == nil || top.Controls.Generation == nil { + return modelError(in.Model) + } + if err := validateImageBridgeControls("request", top.PublicID, top.Controls.Generation, in.Controls); err != nil { + return plan, "invalid_image_control", err.Error() + } + plan.Model, plan.StripCount = top.PublicID, in.Controls.Count.Present + } + for _, tool := range in.Tools { + if tool.InvalidModel { + return plan, "invalid_image_control", fmt.Sprintf("tools[%d].model must be a bounded string", tool.Index) + } + model := tool.Model + if model == "" { + model = "gpt-image-2" + } + capability, ok := resolve(model) + if !ok || !available(capability) || !supports(capability) || capability.Controls == nil || capability.Controls.Generation == nil { + return modelError(model) + } + if err := validateImageBridgeControls(fmt.Sprintf("tools[%d]", tool.Index), capability.PublicID, capability.Controls.Generation, tool.Controls); err != nil { + return plan, "invalid_image_control", err.Error() + } + plan.Tools = append(plan.Tools, extensionv1.ImageBridgeModelPlan{Index: tool.Index, Model: capability.LiveUpstreamID, StripCount: tool.Controls.Count.Present}) + } + return plan, "", "" +} + +func validateImageBridgeControls(location, model string, controls *extensionv1.CindyImageRequestControls, facts extensionv1.ImageBridgeControls) error { + for _, field := range []struct { + name string + value extensionv1.ImageBridgeValue + allowed []string + }{{"size", facts.Size, controls.Sizes}, {"quality", facts.Quality, controls.Qualities}} { + if field.value.Present && (!field.value.Valid || !imageControlAllows(field.allowed, field.value.Text)) { + return fmt.Errorf("%s.%s is not verified for model %q", location, field.name, model) + } + } + count := facts.Count + if count.Present && (!count.Valid || count.Number != math.Trunc(count.Number) || count.Number < 1 || count.Number > float64(controls.MaxOutputCount)) { + return fmt.Errorf("%s.n must be between 1 and %d for model %q", location, controls.MaxOutputCount, model) + } + if len(facts.Present) > 0 { + return fmt.Errorf("%s.%s is not verified for model %q", location, facts.Present[0], model) + } + return nil +} + +func imageControlAllows(allowed []string, value string) bool { + value = strings.TrimSpace(value) + if value == "" { + return false + } + for _, candidate := range allowed { + if strings.EqualFold(strings.TrimSpace(candidate), value) { + return true + } + } + return false +} + +func planImageStudioRequest(in extensionv1.ImageStudioPlanRequest) (extensionv1.ImageStudioPlan, string, string) { + plan := extensionv1.ImageStudioPlan{Model: strings.TrimSpace(in.Model), Mode: in.Mode, Size: strings.TrimSpace(in.Size), Quality: strings.TrimSpace(in.Quality), OutputPerRequest: 1, ResponseFormat: "b64_json", Endpoint: "/v1/images/generations"} + if !in.APIKeySelected { + return plan, "invalid_api_key", "Select an Image Studio API key" + } + if in.PromptBytes <= 0 || in.PromptBytes > 32000 { + return plan, "invalid_prompt", "Prompt must contain between 1 and 32000 bytes" + } + if in.Count < 1 || in.Count > extensionv1.ImageStudioMaxOutputCount { + return plan, "invalid_count", "Image count must be between 1 and 4" + } + if plan.Model != extensionv1.ImageStudioModelGPTImage2 && plan.Model != extensionv1.ImageStudioModelGeminiProImage { + return plan, "unsupported_model", "Image Studio model is not supported" + } + if in.Mode != "generate" && in.Mode != "edit" { + return plan, "unsupported_mode", "Image Studio mode is not supported" + } + if in.Mode == "edit" && plan.Model != extensionv1.ImageStudioModelGeminiProImage { + return plan, "unsupported_mode", "This model does not support image editing" + } + if in.Mode == "edit" && !in.HasReference { + return plan, "reference_required", "A reference image is required for editing" + } + if in.Mode == "generate" && in.HasReference { + return plan, "reference_not_allowed", "Reference images are only supported for editing" + } + if in.HasMask && (in.Mode != "edit" || plan.Model != extensionv1.ImageStudioModelGeminiProImage) { + return plan, "mask_not_allowed", "Masks are not supported for this request" + } + if plan.Size != "" && plan.Size != "1024x1024" { + return plan, "unsupported_size", "Image size is not supported" + } + if plan.Quality != "" && plan.Quality != "low" { + return plan, "unsupported_quality", "Image quality is not supported" + } + if plan.Size == "" { + plan.Size = "1024x1024" + } + if plan.Quality == "" { + plan.Quality = "low" + } + if in.Mode == "edit" { + plan.Endpoint = "/v1/images/edits" + } + return plan, "", "" +} + +func imageStudioModelChoices(capabilities []extensionv1.CindyModelCapability) []extensionv1.CindyModelCapability { + result := make([]extensionv1.CindyModelCapability, 0, 2) + for _, model := range []string{extensionv1.ImageStudioModelGPTImage2, extensionv1.ImageStudioModelGeminiProImage} { + for _, capability := range capabilities { + if capability.ID != model || capability.Kind != extensionv1.CindyModelKindImage { + continue + } + if capability.Controls != nil { + capability.Controls = &extensionv1.CindyCapabilityControls{Generation: extensionv1.CloneCindyImageRequestControls(capability.Controls.Generation), Edit: extensionv1.CloneCindyImageRequestControls(capability.Controls.Edit)} + if capability.Controls.Generation != nil { + capability.Controls.Generation.MaxOutputCount = extensionv1.ImageStudioMaxOutputCount + } + if capability.Controls.Edit != nil { + capability.Controls.Edit.MaxOutputCount = extensionv1.ImageStudioMaxOutputCount + } + } + result = append(result, capability) + break + } + } + return result +} diff --git a/backend/internal/service/image_tools_policy_test.go b/backend/internal/service/image_tools_policy_test.go new file mode 100644 index 000000000000..6f01be13e516 --- /dev/null +++ b/backend/internal/service/image_tools_policy_test.go @@ -0,0 +1,126 @@ +package service + +import ( + "context" + "testing" + + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +func imageBridgeFixture() extensionv1.ImageBridgeRequest { + return extensionv1.ImageBridgeRequest{Stage: "validate", Model: "luna", CatalogEnabled: true, + Capabilities: []extensionv1.CindyCapability{{PublicID: "gpt-image-2", LiveUpstreamID: "openai/gpt-image-2", PublicModel: true, + Kind: extensionv1.CindyModelKindImage, VerifiedEndpoints: []extensionv1.CindyEndpoint{extensionv1.CindyEndpointImagesGenerate}, + Controls: &extensionv1.CindyCapabilityControls{Generation: &extensionv1.CindyImageRequestControls{Sizes: []string{"1024x1024"}, Qualities: []string{"low"}, MaxOutputCount: 1}}}}, + Tools: []extensionv1.ImageBridgeTool{{Index: 2, Controls: extensionv1.ImageBridgeControls{Count: extensionv1.ImageBridgeValue{Present: true, Valid: true, Number: 1}}}}, + } +} + +func TestImageToolsResponsesBridgePlansOnlyVerifiedModelsAndRequiresEnablement(t *testing.T) { + in := imageBridgeFixture() + enabled := extensionv1.ImageToolsConfig{ResponsesImageEnabled: true} + plan, code, message := planResponsesImageBridge(in, enabled) + if code != "" || len(plan.Tools) != 1 || plan.Tools[0].Index != 2 || plan.Tools[0].Model != "openai/gpt-image-2" || !plan.Tools[0].StripCount { + t.Fatalf("default tool plan: %+v %s %s", plan, code, message) + } + for _, disabled := range []extensionv1.ImageToolsConfig{{}, {StudioEnabled: true}} { + if _, code, _ := planResponsesImageBridge(in, disabled); code != "image_model_not_found" { + t.Fatalf("disabled bridge executed with config %+v: %s", disabled, code) + } + } + in.Model = "openai/gpt-image-2" + in.Tools = nil + plan, code, _ = planResponsesImageBridge(in, enabled) + if code != "" || !plan.Supported || plan.Model != "gpt-image-2" { + t.Fatalf("top-level live ID: %+v %s", plan, code) + } + in.Capabilities[0].PublicModel = false + if _, code, _ := planResponsesImageBridge(in, enabled); code != "image_model_not_found" { + t.Fatalf("hidden model admitted: %s", code) + } +} + +func TestImageToolsResponsesBridgeRejectsUnverifiedControlsBeforeIssuingPlan(t *testing.T) { + for _, count := range []float64{0, 2, 1.5} { + in := imageBridgeFixture() + in.Tools[0].Controls.Count.Number = count + if _, code, _ := planResponsesImageBridge(in, extensionv1.ImageToolsConfig{ResponsesImageEnabled: true}); code != "invalid_image_control" { + t.Fatalf("invalid count %v: %s", count, code) + } + } + for _, field := range []string{"quality", "size", "mask", "model"} { + in := imageBridgeFixture() + switch field { + case "quality": + in.Tools[0].Controls.Quality = extensionv1.ImageBridgeValue{Present: true, Valid: true, Text: "high"} + case "size": + in.Tools[0].Controls.Size = extensionv1.ImageBridgeValue{Present: true, Valid: false} + case "mask": + in.Tools[0].Controls.Present = []string{"mask"} + case "model": + in.Tools[0].InvalidModel = true + } + if _, code, _ := planResponsesImageBridge(in, extensionv1.ImageToolsConfig{ResponsesImageEnabled: true}); code != "invalid_image_control" { + t.Fatalf("invalid %s: %s", field, code) + } + } +} + +func TestImageToolsNativeImageControlsKeepGenerationAndEditingIndependent(t *testing.T) { + request := extensionv1.ImageNativeRequest{Model: "fixture-image", Verified: true, Count: 1, Size: "1024x1024", Quality: "low", ResponseFormat: "b64_json", Capability: extensionv1.CindyCapability{PublicID: "fixture-image", PublicModel: true, Kind: extensionv1.CindyModelKindImage, Controls: &extensionv1.CindyCapabilityControls{Generation: &extensionv1.CindyImageRequestControls{Sizes: []string{"1024x1024"}, Qualities: []string{"low"}, MaxOutputCount: 1}}}} + if err := validateNativeImageRequest(request); err != nil { + t.Fatal(err) + } + for _, change := range []func(*extensionv1.ImageNativeRequest){ + func(r *extensionv1.ImageNativeRequest) { r.Editing = true; r.HasReference = true }, + func(r *extensionv1.ImageNativeRequest) { r.Count = 2 }, + func(r *extensionv1.ImageNativeRequest) { r.Stream = true }, + func(r *extensionv1.ImageNativeRequest) { r.UnverifiedControls = true }, + func(r *extensionv1.ImageNativeRequest) { r.Verified = false }, + } { + copy := request + change(©) + if err := validateNativeImageRequest(copy); err == nil { + t.Fatalf("unverified request admitted: %+v", copy) + } + } + request.Editing = true + request.Capability.Controls.Edit = &extensionv1.CindyImageRequestControls{Sizes: []string{"1024x1024"}, Qualities: []string{"low"}, MaxOutputCount: 1, SupportsReferenceImage: true} + if err := validateNativeImageRequest(request); err == nil { + t.Fatal("missing reference admitted") + } + request.HasReference = true + if err := validateNativeImageRequest(request); err != nil { + t.Fatal(err) + } + request.HasMask = true + if err := validateNativeImageRequest(request); err == nil { + t.Fatal("unverified mask admitted") + } +} + +func TestImageToolsStudioPlanRequiresExplicitEnablementAndKeepsNativeOne(t *testing.T) { + t.Cleanup(func() { ConfigureImageTools(nil) }) + input := ImageStudioCreateInput{APIKeyID: 1, Mode: "generate", Model: "gpt-image-2", Prompt: "fixture text", Count: 4} + ConfigureImageTools(&extensionv1.ImageToolsConfig{}) + if _, err := planImageStudio(context.Background(), input, false, false, true); err == nil { + t.Fatal("default must remain disabled") + } + if _, err := planImageStudio(context.Background(), input, false, false, false); err != nil { + t.Fatalf("validation must not require enablement: %v", err) + } + ConfigureImageTools(&extensionv1.ImageToolsConfig{StudioEnabled: true}) + plan, err := planImageStudio(context.Background(), input, false, false, true) + if err != nil || plan.OutputPerRequest != 1 || plan.Size != "1024x1024" || plan.Endpoint != "/v1/images/generations" { + t.Fatalf("unexpected plan: %+v %v", plan, err) + } + input.Mode = "edit" + if _, err := planImageStudio(context.Background(), input, true, false, true); err == nil { + t.Fatal("unsupported editing must be rejected") + } + input.Model = "gemini-3-pro-image" + plan, err = planImageStudio(context.Background(), input, true, false, true) + if err != nil || plan.Endpoint != "/v1/images/edits" { + t.Fatalf("supported editing plan unavailable: %+v %v", plan, err) + } +} diff --git a/backend/internal/service/openai_gateway_forward.go b/backend/internal/service/openai_gateway_forward.go index 521f04c7422f..d3bc6f389bc0 100644 --- a/backend/internal/service/openai_gateway_forward.go +++ b/backend/internal/service/openai_gateway_forward.go @@ -13,7 +13,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" ) @@ -23,13 +22,6 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco rememberPromptRequestedModel(c, body) stageCodexRoutingTurn(c, body) if account != nil && IsCindyAPIKeyAccount(account.Platform, account.Type, account.Credentials) && IsImageGenerationIntent(openAIResponsesEndpoint, gjson.GetBytes(body, "model").String(), body) { - bound, release, err := bindProcessExtensionContext(ctx, PlatformCindy, AccountTypeAPIKey, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.responses.plan", AccountID: account.ID}) - if err != nil { - c.JSON(http.StatusServiceUnavailable, gin.H{"error": gin.H{"type": "service_unavailable", "message": "Image bridge is unavailable"}}) - return nil, err - } - defer release() - ctx = bound resolved, err := ResolveCindyResponsesImageToolsForAccount(ctx, account, body) if err != nil { status, code, message := http.StatusBadRequest, "invalid_request_error", "Invalid image bridge request" diff --git a/backend/internal/service/openai_images.go b/backend/internal/service/openai_images.go index ab3317e15912..f824cbf05467 100644 --- a/backend/internal/service/openai_images.go +++ b/backend/internal/service/openai_images.go @@ -22,7 +22,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/util/responseheaders" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/gin-gonic/gin" "github.com/imroc/req/v3" "github.com/tidwall/gjson" @@ -687,14 +686,6 @@ func (s *OpenAIGatewayService) ForwardImages( parsed *OpenAIImagesRequest, channelMappedModel string, ) (*OpenAIForwardResult, error) { - if parsed != nil && account != nil && IsCindyAPIKeyAccount(account.Platform, account.Type, account.Credentials) { - bound, release, err := bindProcessExtensionContext(ctx, PlatformCindy, AccountTypeAPIKey, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.native.validate", AccountID: account.ID}) - if err != nil { - return nil, err - } - defer release() - ctx = bound - } pricingContext, pricingErr := CaptureCindyPricingContext(ctx, c, account) if pricingErr != nil { return nil, pricingErr diff --git a/backend/internal/service/plugin_extension_runtime_test.go b/backend/internal/service/plugin_extension_runtime_test.go index 4a9f083bf6a9..2986fc6a0c10 100644 --- a/backend/internal/service/plugin_extension_runtime_test.go +++ b/backend/internal/service/plugin_extension_runtime_test.go @@ -149,44 +149,6 @@ func TestObservabilityExtensionRuntimeUsesScopedMetricsBroker(t *testing.T) { require.Zero(t, directory.calls, "metrics access must not resolve credentials") } -func TestImageToolsExtensionRuntimeUsesIndependentProcess(t *testing.T) { - binary := os.Getenv("SUB2API_IMAGE_TOOLS_TEST_BINARY") - if binary == "" { - t.Skip("set SUB2API_IMAGE_TOOLS_TEST_BINARY to the independent image tools program") - } - data, err := os.ReadFile(binary) - require.NoError(t, err) - digest := sha256.Sum256(data) - installation := &PluginInstallation{ID: 12, PluginKey: "codexrip.image-tools", Version: firstPartyTestVersion(t, "image-tools"), BinaryPath: binary, BinarySHA256: hex.EncodeToString(digest[:]), Manifest: PluginManifest{Requires: PluginRequirements{ExtensionAPI: 1}, Capabilities: []PluginCapability{{ID: extensionv1.CapabilityRequest, Platform: "*", AccountType: "*"}}}} - host := newPluginHostServiceServer(installation.PluginKey, nil, nil, PluginAccountScope{}) - host.extension = &pluginExtensionHost{key: installation.PluginKey, state: &extensionReadOnlyFixture{}} - socketDir := filepath.Join(t.TempDir(), "runtime") - require.NoError(t, os.MkdirAll(socketDir, 0700)) - runtime, err := startPluginRuntime(context.Background(), installation, 15*time.Second, socketDir, host) - require.NoError(t, err) - t.Cleanup(runtime.kill) - require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{}`))) - raw, _ := json.Marshal(extensionv1.ImageStudioPlanRequest{APIKeySelected: true, Mode: "generate", Model: "gpt-image-2", PromptBytes: 5, Count: 4}) - invocation := extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.studio.plan", Payload: raw} - out, err := runtime.extension.Invoke(context.Background(), invocation) - require.NoError(t, err) - require.Equal(t, "studio_disabled", out.Code) - require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{"studio_enabled":true}`))) - out, err = runtime.extension.Invoke(context.Background(), invocation) - require.NoError(t, err) - require.Empty(t, out.Code) - var plan extensionv1.ImageStudioPlan - require.NoError(t, json.Unmarshal(out.Payload, &plan)) - require.Equal(t, 1, plan.OutputPerRequest) - require.Equal(t, "/v1/images/generations", plan.Endpoint) - // Exercise the new operation through the actual process transport. Only - // synthetic provider metadata crosses the RPC, without any upstream client. - raw, _ = json.Marshal(extensionv1.ImageBridgeRequest{Stage: "validate", Model: "controller", Tools: []extensionv1.ImageBridgeTool{{Index: 0}}}) - out, err = runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.responses.plan", AccountID: 9, Payload: raw}) - require.NoError(t, err) - require.Equal(t, "image_model_not_found", out.Code) -} - func TestCindyProviderExtensionRuntimeUsesIndependentProcess(t *testing.T) { binary := os.Getenv("SUB2API_CINDY_PROVIDER_TEST_BINARY") if binary == "" { diff --git a/backend/internal/service/plugin_feature_decode_test.go b/backend/internal/service/plugin_feature_decode_test.go index f914ffdf3a1e..0fe1cfe6b633 100644 --- a/backend/internal/service/plugin_feature_decode_test.go +++ b/backend/internal/service/plugin_feature_decode_test.go @@ -32,19 +32,6 @@ func TestPluginFeatureConfigsDiscardPartiallyDecodedFlags(t *testing.T) { require.False(t, CindyBalanceDetectionFeatureEnabled()) require.False(t, CindySearchFeatureEnabled()) }) - for _, payload := range []string{ - `{"studio_enabled":true,"responses_image_enabled":"invalid"}`, - `{"studio_enabled":"invalid","responses_image_enabled":true}`, - } { - t.Run(payload, func(t *testing.T) { - withFeatureDecodeFixture(t, payload) - value, ok := currentImageToolsConfig() - require.False(t, ok) - require.Equal(t, extensionv1.ImageToolsConfig{}, value) - require.False(t, ImageStudioFeatureEnabled()) - require.False(t, CindyResponsesImageBridgeFeatureEnabled()) - }) - } } func TestCindyResponseDecisionDiscardsPartialDecode(t *testing.T) { @@ -66,10 +53,4 @@ func TestPluginFeatureConfigsKeepValidExplicitFlags(t *testing.T) { require.True(t, ok) require.Equal(t, extensionv1.CindyProviderConfig{BalanceDetection: true, SearchEnabled: true}, value) }) - t.Run("image", func(t *testing.T) { - withFeatureDecodeFixture(t, `{"studio_enabled":true,"responses_image_enabled":false}`) - value, ok := currentImageToolsConfig() - require.True(t, ok) - require.Equal(t, extensionv1.ImageToolsConfig{StudioEnabled: true}, value) - }) } diff --git a/backend/internal/service/plugin_image_bridge.go b/backend/internal/service/plugin_image_bridge.go index 742dc95e691b..353961f30331 100644 --- a/backend/internal/service/plugin_image_bridge.go +++ b/backend/internal/service/plugin_image_bridge.go @@ -7,7 +7,6 @@ import ( "fmt" "math" "strings" - "time" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) @@ -80,28 +79,15 @@ func queryImageBridgeWithSnapshot(ctx context.Context, account *Account, request request.Capabilities = snapshot.Capabilities request.Aliases = snapshot.CompatibilityAliases request.CatalogEnabled = snapshot.Config.CatalogEnabled - raw, err := json.Marshal(request) - var plan extensionv1.ImageBridgePlan - if err != nil { - return plan, err - } - in := extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.responses.plan", Payload: raw} - if account != nil { - in.AccountID = account.ID - } - call, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - result, err := invokeProcessExtensionCached(call, PlatformCindy, AccountTypeAPIKey, in) - if err != nil { - return plan, err - } - if result.Code == "image_model_not_found" { - return plan, fmt.Errorf("%w: %s", ErrCindyResponsesImageToolModelNotFound, result.Message) + config, _ := currentImageToolsConfig() + plan, code, message := planResponsesImageBridge(request, config) + if code == "image_model_not_found" { + return plan, fmt.Errorf("%w: %s", ErrCindyResponsesImageToolModelNotFound, message) } - if result.Code != "" { - return plan, errors.New(result.Message) + if code != "" { + return plan, errors.New(message) } - if json.Unmarshal(result.Payload, &plan) != nil || !validImageBridgePlan(request, plan) { + if !validImageBridgePlan(request, plan) { return plan, ErrExtensionOperationUnavailable } return plan, nil diff --git a/backend/internal/service/plugin_image_bridge_test.go b/backend/internal/service/plugin_image_bridge_test.go index aced28b01b0c..6cd5078c7bad 100644 --- a/backend/internal/service/plugin_image_bridge_test.go +++ b/backend/internal/service/plugin_image_bridge_test.go @@ -11,51 +11,14 @@ import ( "github.com/stretchr/testify/require" ) -type capturedImageBridgeFixture struct { - promptPolicyFixture - queries []extensionv1.Invocation -} - -func (f *capturedImageBridgeFixture) InvokeOperation(ctx context.Context, platform, kind string, in extensionv1.Invocation) (extensionv1.Result, error) { - if in.Operation == "image.responses.plan" || in.Operation == "image.native.validate" { - f.queries = append(f.queries, in) - } - return f.promptPolicyFixture.InvokeOperation(ctx, platform, kind, in) -} - -func TestCindyNativeImagePolicyReceivesNoPromptOrImageContent(t *testing.T) { +func TestImageBridgeUsesOnlyBoundedFacts(t *testing.T) { previous := processExtensionOperations.Load() t.Cleanup(func() { processExtensionOperations.Store(previous) }) - fixture := &capturedImageBridgeFixture{} - processExtensionOperations.Store(&extensionOperationProvider{invoker: fixture}) - request := &OpenAIImagesRequest{Endpoint: openAIImagesEditsEndpoint, Prompt: "private-image-prompt", InputImageURLs: []string{"https://private.invalid/reference"}, N: 1, Size: "1024x1024", Quality: "low", HasMask: true} - account := cindyHTTPToWSV2TestAccount() - account.ID = 37 - err := ValidateCindyImageRequestForAccount(context.Background(), account, "gpt-image-2", request) - require.Error(t, err) - require.Len(t, fixture.queries, 1) - require.EqualValues(t, 37, fixture.queries[0].AccountID) - require.NotContains(t, string(fixture.queries[0].Payload), "private-image-prompt") - require.NotContains(t, string(fixture.queries[0].Payload), "private.invalid") - processExtensionOperations.Store(nil) - require.ErrorIs(t, ValidateCindyImageRequest("gpt-image-2", request), ErrExtensionOperationDisabled) -} - -func TestImageBridgeUsesOnlyBoundedFactsAndAuthorizedAccount(t *testing.T) { - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) - fixture := &capturedImageBridgeFixture{} - processExtensionOperations.Store(&extensionOperationProvider{invoker: fixture}) body := []byte(`{"model":"gpt-5.6-luna","input":"private-prompt","reasoning":{"encrypted_content":"private-cipher"},"tools":[{"type":"image_generation","model":"gpt-image-2","mask":"private-image","quality":{"text":"private-object"}}]}`) account := cindyHTTPToWSV2TestAccount() account.ID = 37 _, err := ResolveCindyResponsesImageToolsForAccount(context.Background(), account, body) require.ErrorIs(t, err, ErrCindyResponsesImageToolModelNotFound) - require.Len(t, fixture.queries, 1) - require.EqualValues(t, 37, fixture.queries[0].AccountID) - for _, private := range []string{"private-prompt", "private-cipher", "private-image", "private-object"} { - require.NotContains(t, string(fixture.queries[0].Payload), private) - } var request map[string]any require.NoError(t, json.Unmarshal([]byte(`{"model":"gpt-5.6-luna","tools":[{"type":"image_generation"}]}`), &request)) tools, ok := request["tools"].([]any) diff --git a/backend/internal/service/plugin_prompt_fixture_test.go b/backend/internal/service/plugin_prompt_fixture_test.go index 4ab9d07acb21..64ecac9214b0 100644 --- a/backend/internal/service/plugin_prompt_fixture_test.go +++ b/backend/internal/service/plugin_prompt_fixture_test.go @@ -12,7 +12,6 @@ import ( accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" observability "github.com/HTExplicit/sub2api-plugins/adminobservability/policy" - imagetools "github.com/HTExplicit/sub2api-plugins/imagetools/policy" policy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" @@ -56,14 +55,6 @@ func (promptPolicyFixture) InvokeOperation(ctx context.Context, _ string, _ stri if strings.HasPrefix(in.Operation, "observability.") { return observability.New().Invoke(ctx, in) } - if strings.HasPrefix(in.Operation, "image.") { - module := imagetools.New() - raw, _ := json.Marshal(LegacyImageToolsConfig()) - if err := module.ApplyConfig(ctx, raw); err != nil { - return extensionv1.Result{}, err - } - return module.Invoke(ctx, in) - } if strings.HasPrefix(in.Operation, "cindy.") { module := cindy.New() raw, _ := json.Marshal(LegacyCindyProviderConfig()) diff --git a/backend/internal/service/setting_image_tools.go b/backend/internal/service/setting_image_tools.go new file mode 100644 index 000000000000..5b6e2bd6e97b --- /dev/null +++ b/backend/internal/service/setting_image_tools.go @@ -0,0 +1,66 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "strings" + + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +// SettingKeyImageToolsConfig stores the Image Studio and Responses image bridge +// switches formerly kept in the image-tools plugin configuration. +const SettingKeyImageToolsConfig = "image_tools_config" + +func (s *SettingService) storedImageToolsConfig(ctx context.Context) (extensionv1.ImageToolsConfig, bool) { + if s == nil || s.settingRepo == nil { + return extensionv1.ImageToolsConfig{}, false + } + dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) + defer cancel() + raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyImageToolsConfig) + if err != nil || strings.TrimSpace(raw) == "" { + return extensionv1.ImageToolsConfig{}, false + } + var stored extensionv1.ImageToolsConfig + if json.Unmarshal([]byte(raw), &stored) != nil { + return extensionv1.ImageToolsConfig{}, false + } + return stored, true +} + +// GetImageToolsConfig returns the stored switches, or the deploy-time rollout +// flags when the administrator has not saved them yet. +func (s *SettingService) GetImageToolsConfig(ctx context.Context) extensionv1.ImageToolsConfig { + if stored, ok := s.storedImageToolsConfig(ctx); ok { + return stored + } + return LegacyImageToolsConfig() +} + +// LoadImageToolsConfig installs the stored switches for this process at startup. +func (s *SettingService) LoadImageToolsConfig(ctx context.Context) { + if stored, ok := s.storedImageToolsConfig(ctx); ok { + ConfigureImageTools(&stored) + } +} + +// UpdateImageToolsConfig persists the switches and applies them to this process. +// Image Studio's background runtime still starts only at boot, as before. +func (s *SettingService) UpdateImageToolsConfig(ctx context.Context, config extensionv1.ImageToolsConfig) error { + if s == nil || s.settingRepo == nil { + return errors.New("settings are unavailable") + } + raw, err := json.Marshal(config) + if err != nil { + return err + } + dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) + defer cancel() + if err := s.settingRepo.Set(dbCtx, SettingKeyImageToolsConfig, string(raw)); err != nil { + return err + } + ConfigureImageTools(&config) + return nil +} diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 79de84f3009e..711eca49fde8 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -920,6 +920,8 @@ func ProvideOpsIngressRejectAggregator(opsRepo OpsRepository, opsService *OpsSer // ProvideSettingService wires SettingService with group reader and proxy repo. func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupRepository, proxyRepo ProxyRepository, cfg *config.Config) *SettingService { svc := NewSettingService(settingRepo, cfg) + // Image tool switches are read by package-level gates before Image Studio starts. + svc.LoadImageToolsConfig(context.Background()) svc.SetDefaultSubscriptionGroupReader(groupRepo) svc.SetProxyRepository(proxyRepo) if err := svc.LoadForwardedClientIPSettings(context.Background()); err != nil { diff --git a/backend/internal/testextensions/fixtures.go b/backend/internal/testextensions/fixtures.go index 6c2bf5bd68bd..61803922a394 100644 --- a/backend/internal/testextensions/fixtures.go +++ b/backend/internal/testextensions/fixtures.go @@ -14,17 +14,16 @@ import ( accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" observability "github.com/HTExplicit/sub2api-plugins/adminobservability/policy" - imagetools "github.com/HTExplicit/sub2api-plugins/imagetools/policy" prompt "github.com/HTExplicit/sub2api-plugins/promptskills/policy" "github.com/Wei-Shaw/sub2api/internal/service" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) -type operations struct{ imageConfig *extensionv1.ImageToolsConfig } +type operations struct{} var promptFixtureModule = prompt.New() -func (fixture operations) InvokeOperation(ctx context.Context, _, _ string, in extensionv1.Invocation) (extensionv1.Result, error) { +func (operations) InvokeOperation(ctx context.Context, _, _ string, in extensionv1.Invocation) (extensionv1.Result, error) { if in.Capability == extensionv1.CapabilityRecovery { return codexrecovery.Invoke(ctx, in) } @@ -42,18 +41,6 @@ func (fixture operations) InvokeOperation(ctx context.Context, _, _ string, in e if strings.HasPrefix(in.Operation, "observability.") { return observability.New().Invoke(ctx, in) } - if strings.HasPrefix(in.Operation, "image.") { - module := imagetools.New() - config := service.LegacyImageToolsConfig() - if fixture.imageConfig != nil { - config = *fixture.imageConfig - } - raw, _ := json.Marshal(config) - if err := module.ApplyConfig(ctx, raw); err != nil { - return extensionv1.Result{}, err - } - return module.Invoke(ctx, in) - } if strings.HasPrefix(in.Operation, "cindy.") { module := cindy.New() raw, _ := json.Marshal(service.LegacyCindyProviderConfig()) @@ -70,8 +57,12 @@ func (fixture operations) InvokeOperation(ctx context.Context, _, _ string, in e } return extensionv1.Result{}, service.ErrExtensionOperationDisabled } -func Install() { service.ConfigureProcessExtensionServices(nil, operations{}) } +func Install() { + service.ConfigureProcessExtensionServices(nil, operations{}) + service.ConfigureImageTools(nil) +} func InstallImageTools(config extensionv1.ImageToolsConfig) { - service.ConfigureProcessExtensionServices(nil, operations{imageConfig: &config}) + service.ConfigureProcessExtensionServices(nil, operations{}) + service.ConfigureImageTools(&config) } diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index ac12862200b7..9cb6c7d8aa7a 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -1148,6 +1148,22 @@ export interface OfficialModelCapacityCatalog { entries: OfficialModelCapacityEntry[] } +export interface ImageToolsSettings { + studio_enabled: boolean + responses_image_enabled: boolean +} + +/** Image Studio and Cindy Responses image bridge switches. */ +export async function getImageToolsSettings(): Promise { + const { data } = await apiClient.get("/admin/settings/image-tools"); + return data; +} + +export async function updateImageToolsSettings(settings: ImageToolsSettings): Promise { + const { data } = await apiClient.put("/admin/settings/image-tools", settings); + return data; +} + /** Read-only release-pinned official model capacity catalog. */ export async function getOfficialModelCapacityCatalog(): Promise { const { data } = await apiClient.get("/admin/settings/model-context-catalog"); @@ -1671,6 +1687,8 @@ export async function resetWebSearchUsage(payload: { export const settingsAPI = { getSettings, getOfficialModelCapacityCatalog, + getImageToolsSettings, + updateImageToolsSettings, updateSettings, testSmtpConnection, sendTestEmail, diff --git a/frontend/src/components/admin/ImageToolsSettingsPanel.vue b/frontend/src/components/admin/ImageToolsSettingsPanel.vue new file mode 100644 index 000000000000..7e3d42af626c --- /dev/null +++ b/frontend/src/components/admin/ImageToolsSettingsPanel.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index cc0b9372f41a..df0e2780635a 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -752,7 +752,7 @@ const ChevronDownIcon = { const flagChannelMonitor = makeSidebarFlag(FeatureFlags.channelMonitor) const flagPayment = makeSidebarFlag(FeatureFlags.payment) const flagAvailableChannels = makeSidebarFlag(FeatureFlags.availableChannels) -const flagImageStudio = () => pluginExtensions.items.some(item => item.slot === 'surface' && item.id === 'image-studio') +const flagImageStudio = makeSidebarFlag(FeatureFlags.imageStudio) const flagSubscription = makeSidebarFlag(FeatureFlags.subscription) // 购买入口文案随站点计费模式切换:仅充值 → 「充值」,仅订阅 → 「订阅」,否则「充值/订阅」。 diff --git a/plugins/image-tools/ui/src/App.vue b/frontend/src/features/image-studio/ImageStudioPage.vue similarity index 98% rename from plugins/image-tools/ui/src/App.vue rename to frontend/src/features/image-studio/ImageStudioPage.vue index 331b747669ab..901f7afeeb99 100644 --- a/plugins/image-tools/ui/src/App.vue +++ b/frontend/src/features/image-studio/ImageStudioPage.vue @@ -316,7 +316,13 @@ diff --git a/plugins/bundle.source.json b/plugins/bundle.source.json index 4fd92e6c59ca..a348ecf58bb9 100644 --- a/plugins/bundle.source.json +++ b/plugins/bundle.source.json @@ -5,7 +5,6 @@ { "directory": "prompt-skills", "default_enabled": true }, { "directory": "account-tools", "default_enabled": true }, { "directory": "cindy-provider", "default_enabled": true, "migration": "cindy-provider-v1" }, - { "directory": "image-tools", "default_enabled": true, "migration": "image-tools-v1" }, { "directory": "admin-observability", "default_enabled": true, "migration": "admin-observability-v1" } ] } diff --git a/plugins/image-tools/cmd/plugin/main.go b/plugins/image-tools/cmd/plugin/main.go deleted file mode 100644 index 7f5003bc6951..000000000000 --- a/plugins/image-tools/cmd/plugin/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "github.com/HTExplicit/sub2api-plugins/imagetools/policy" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -var version = "0.2.11" - -func main() { - extensionv1.Serve(extensionv1.Definition{ID: "codexrip.image-tools", Version: version, Capabilities: []string{extensionv1.CapabilityRequest, extensionv1.CapabilityAdmin}}, policy.New()) -} diff --git a/plugins/image-tools/go.mod b/plugins/image-tools/go.mod deleted file mode 100644 index 727fc9a0e09b..000000000000 --- a/plugins/image-tools/go.mod +++ /dev/null @@ -1,24 +0,0 @@ -module github.com/HTExplicit/sub2api-plugins/imagetools - -go 1.27.0 - -require github.com/Wei-Shaw/sub2api v0.0.0 - -require ( - github.com/fatih/color v1.18.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-plugin v1.8.0 // indirect - github.com/hashicorp/yamux v0.1.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/run v1.1.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.2 // indirect - google.golang.org/protobuf v1.36.11 // indirect -) - -replace github.com/Wei-Shaw/sub2api => ../../backend diff --git a/plugins/image-tools/go.sum b/plugins/image-tools/go.sum deleted file mode 100644 index 76372f7247c2..000000000000 --- a/plugins/image-tools/go.sum +++ /dev/null @@ -1,83 +0,0 @@ -github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= -github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= -github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= -github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= -github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= -github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/image-tools/manifest.source.json b/plugins/image-tools/manifest.source.json deleted file mode 100644 index 884fe5de01eb..000000000000 --- a/plugins/image-tools/manifest.source.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "schema_version": 1, - "id": "codexrip.image-tools", - "name": "图像工具", - "version": "0.2.13", - "description": "Image Studio 请求计划、模型能力和 Responses 图像桥接策略", - "author": "HTExplicit", - "requires": { "sub2api": ">=0.2.7-codexrip.2 <0.2.9", "tested_sub2api_versions": [], "plugin_protocol": 1, "transport_api": 1, "ui_bridge": 1, "extension_api": 1 }, - "capabilities": [ - { "id": "extensions.request.v1", "platform": "*", "account_type": "*" }, - { "id": "extensions.admin.v1", "platform": "*", "account_type": "*" } - ], - "dependencies": [{ "capability": "extensions.provider.v1", "platform": "cindy", "account_type": "apikey" }], - "operations": { "extensions.request.v1": ["image.features", "image.studio.plan", "image.studio.validate", "image.studio.models", "image.responses.plan", "image.native.validate"] }, - "resources": [ - { "name": "image.keys", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.create", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.jobs", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.job", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.items", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.cancel", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.retry", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.artifact", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.history.list", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.history.save", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.history.delete", "capability": "extensions.request.v1", "permission": "user" }, - { "name": "image.history.clear", "capability": "extensions.request.v1", "permission": "user" } - ], - "contributions": [ - { "id": "image-studio", "slot": "surface", "capability": "extensions.request.v1", "entrypoint": "ui/compiled/index.html", "retained_controls": true, "config_flag": "studio_enabled", "permission": "user", "label": { "zh": "Image Studio", "en": "Image Studio" } }, - { "id": "image-tools-settings", "slot": "admin.settings", "capability": "extensions.admin.v1", "action": "image.describe", "entrypoint": "ui/index.html", "permission": "admin", "label": { "zh": "图像工具", "en": "Image tools" } } - ], - "runtimes": {}, - "ui": { "entrypoint": "ui/index.html" }, - "files": {} -} diff --git a/plugins/image-tools/policy/bridge.go b/plugins/image-tools/policy/bridge.go deleted file mode 100644 index 9cdf12111df9..000000000000 --- a/plugins/image-tools/policy/bridge.go +++ /dev/null @@ -1,122 +0,0 @@ -package policy - -import ( - "fmt" - "math" - "slices" - "strings" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -func planResponsesBridge(in extensionv1.ImageBridgeRequest, config extensionv1.ImageToolsConfig) (extensionv1.ImageBridgePlan, string, string) { - plan := extensionv1.ImageBridgePlan{} - resolve := func(model string) (extensionv1.CindyCapability, bool) { - if target, ok := in.Aliases[model]; ok { - model = target - } - for _, capability := range in.Capabilities { - if model == capability.PublicID || model == capability.LiveUpstreamID { - return capability, true - } - } - return extensionv1.CindyCapability{}, false - } - available := func(c extensionv1.CindyCapability) bool { - return c.PublicModel && (in.CatalogEnabled || - (c.Kind == extensionv1.CindyModelKindImage && config.StudioEnabled) || - (c.PublicID == "gpt-image-2" && config.ResponsesImageEnabled)) - } - supports := func(c extensionv1.CindyCapability) bool { - return config.ResponsesImageEnabled && c.PublicModel && c.PublicID == "gpt-image-2" && - c.Kind == extensionv1.CindyModelKindImage && slices.Contains(c.VerifiedEndpoints, extensionv1.CindyEndpointImagesGenerate) - } - top, known := resolve(in.Model) - plan.Supported = known && supports(top) - if in.Stage == "supports" { - return plan, "", "" - } - if in.Stage == "map" { - if len(in.Tools) > 0 && !config.ResponsesImageEnabled { - return plan, "image_model_not_found", "Responses image bridge is disabled" - } - if known && available(top) { - plan.Model = top.LiveUpstreamID - } - for _, tool := range in.Tools { - if capability, ok := resolve(tool.Model); ok && available(capability) && capability.Kind == extensionv1.CindyModelKindImage { - plan.Tools = append(plan.Tools, extensionv1.ImageBridgeModelPlan{Index: tool.Index, Model: capability.LiveUpstreamID}) - } - } - return plan, "", "" - } - if in.Stage != "validate" { - return plan, "invalid_bridge_stage", "Invalid image bridge stage" - } - modelError := func(model string) (extensionv1.ImageBridgePlan, string, string) { - return plan, "image_model_not_found", fmt.Sprintf("cindy Responses image tool model is not verified: %q", model) - } - if known && top.Kind == extensionv1.CindyModelKindImage && !plan.Supported { - return modelError(in.Model) - } - if plan.Supported { - if top.Controls == nil || top.Controls.Generation == nil { - return modelError(in.Model) - } - if err := validateBridgeControls("request", top.PublicID, top.Controls.Generation, in.Controls); err != nil { - return plan, "invalid_image_control", err.Error() - } - plan.Model, plan.StripCount = top.PublicID, in.Controls.Count.Present - } - for _, tool := range in.Tools { - if tool.InvalidModel { - return plan, "invalid_image_control", fmt.Sprintf("tools[%d].model must be a bounded string", tool.Index) - } - model := tool.Model - if model == "" { - model = "gpt-image-2" - } - capability, ok := resolve(model) - if !ok || !available(capability) || !supports(capability) || capability.Controls == nil || capability.Controls.Generation == nil { - return modelError(model) - } - if err := validateBridgeControls(fmt.Sprintf("tools[%d]", tool.Index), capability.PublicID, capability.Controls.Generation, tool.Controls); err != nil { - return plan, "invalid_image_control", err.Error() - } - plan.Tools = append(plan.Tools, extensionv1.ImageBridgeModelPlan{Index: tool.Index, Model: capability.LiveUpstreamID, StripCount: tool.Controls.Count.Present}) - } - return plan, "", "" -} - -func validateBridgeControls(location, model string, controls *extensionv1.CindyImageRequestControls, facts extensionv1.ImageBridgeControls) error { - for _, field := range []struct { - name string - value extensionv1.ImageBridgeValue - allowed []string - }{{"size", facts.Size, controls.Sizes}, {"quality", facts.Quality, controls.Qualities}} { - if field.value.Present && (!field.value.Valid || !imageControlAllows(field.allowed, field.value.Text)) { - return fmt.Errorf("%s.%s is not verified for model %q", location, field.name, model) - } - } - count := facts.Count - if count.Present && (!count.Valid || count.Number != math.Trunc(count.Number) || count.Number < 1 || count.Number > float64(controls.MaxOutputCount)) { - return fmt.Errorf("%s.n must be between 1 and %d for model %q", location, controls.MaxOutputCount, model) - } - if len(facts.Present) > 0 { - return fmt.Errorf("%s.%s is not verified for model %q", location, facts.Present[0], model) - } - return nil -} - -func imageControlAllows(allowed []string, value string) bool { - value = strings.TrimSpace(value) - if value == "" { - return false - } - for _, candidate := range allowed { - if strings.EqualFold(strings.TrimSpace(candidate), value) { - return true - } - } - return false -} diff --git a/plugins/image-tools/policy/bridge_test.go b/plugins/image-tools/policy/bridge_test.go deleted file mode 100644 index b7161e3ef924..000000000000 --- a/plugins/image-tools/policy/bridge_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package policy - -import ( - "testing" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -func bridgeFixture() extensionv1.ImageBridgeRequest { - return extensionv1.ImageBridgeRequest{Stage: "validate", Model: "luna", CatalogEnabled: true, - Capabilities: []extensionv1.CindyCapability{{PublicID: "gpt-image-2", LiveUpstreamID: "openai/gpt-image-2", PublicModel: true, - Kind: extensionv1.CindyModelKindImage, VerifiedEndpoints: []extensionv1.CindyEndpoint{extensionv1.CindyEndpointImagesGenerate}, - Controls: &extensionv1.CindyCapabilityControls{Generation: &extensionv1.CindyImageRequestControls{Sizes: []string{"1024x1024"}, Qualities: []string{"low"}, MaxOutputCount: 1}}}}, - Tools: []extensionv1.ImageBridgeTool{{Index: 2, Controls: extensionv1.ImageBridgeControls{Count: extensionv1.ImageBridgeValue{Present: true, Valid: true, Number: 1}}}}, - } -} - -func TestResponsesBridgePlansOnlyVerifiedModelsAndRequiresEnablement(t *testing.T) { - in := bridgeFixture() - enabled := extensionv1.ImageToolsConfig{ResponsesImageEnabled: true} - plan, code, message := planResponsesBridge(in, enabled) - if code != "" || len(plan.Tools) != 1 || plan.Tools[0].Index != 2 || plan.Tools[0].Model != "openai/gpt-image-2" || !plan.Tools[0].StripCount { - t.Fatalf("default tool plan: %+v %s %s", plan, code, message) - } - for _, disabled := range []extensionv1.ImageToolsConfig{{}, {StudioEnabled: true}} { - if _, code, _ := planResponsesBridge(in, disabled); code != "image_model_not_found" { - t.Fatalf("disabled bridge executed with config %+v: %s", disabled, code) - } - } - in.Model = "openai/gpt-image-2" - in.Tools = nil - plan, code, _ = planResponsesBridge(in, enabled) - if code != "" || !plan.Supported || plan.Model != "gpt-image-2" { - t.Fatalf("top-level live ID: %+v %s", plan, code) - } - in.Capabilities[0].PublicModel = false - if _, code, _ := planResponsesBridge(in, enabled); code != "image_model_not_found" { - t.Fatalf("hidden model admitted: %s", code) - } -} - -func TestResponsesBridgeRejectsUnverifiedControlsBeforeIssuingPlan(t *testing.T) { - for _, count := range []float64{0, 2, 1.5} { - in := bridgeFixture() - in.Tools[0].Controls.Count.Number = count - if _, code, _ := planResponsesBridge(in, extensionv1.ImageToolsConfig{ResponsesImageEnabled: true}); code != "invalid_image_control" { - t.Fatalf("invalid count %v: %s", count, code) - } - } - for _, field := range []string{"quality", "size", "mask", "model"} { - in := bridgeFixture() - switch field { - case "quality": - in.Tools[0].Controls.Quality = extensionv1.ImageBridgeValue{Present: true, Valid: true, Text: "high"} - case "size": - in.Tools[0].Controls.Size = extensionv1.ImageBridgeValue{Present: true, Valid: false} - case "mask": - in.Tools[0].Controls.Present = []string{"mask"} - case "model": - in.Tools[0].InvalidModel = true - } - if _, code, _ := planResponsesBridge(in, extensionv1.ImageToolsConfig{ResponsesImageEnabled: true}); code != "invalid_image_control" { - t.Fatalf("invalid %s: %s", field, code) - } - } -} diff --git a/plugins/image-tools/policy/module.go b/plugins/image-tools/policy/module.go deleted file mode 100644 index 75597939de27..000000000000 --- a/plugins/image-tools/policy/module.go +++ /dev/null @@ -1,182 +0,0 @@ -package policy - -import ( - "context" - "encoding/json" - "errors" - "strings" - "sync" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -type Module struct { - mu sync.RWMutex - config extensionv1.ImageToolsConfig -} - -func New() *Module { return &Module{} } -func (m *Module) ValidateConfig(_ context.Context, raw json.RawMessage) (json.RawMessage, error) { - var fields map[string]json.RawMessage - var cfg extensionv1.ImageToolsConfig - if json.Unmarshal(raw, &fields) != nil || fields == nil || json.Unmarshal(raw, &cfg) != nil { - return nil, errors.New("invalid image tools configuration") - } - for key, value := range fields { - if (key != "studio_enabled" && key != "responses_image_enabled") || (string(value) != "true" && string(value) != "false") { - return nil, errors.New("unknown or invalid image tools setting") - } - } - return json.Marshal(cfg) -} -func (m *Module) ApplyConfig(ctx context.Context, raw json.RawMessage) error { - normalized, err := m.ValidateConfig(ctx, raw) - if err != nil { - return err - } - var cfg extensionv1.ImageToolsConfig - if err := json.Unmarshal(normalized, &cfg); err != nil { - return err - } - m.mu.Lock() - m.config = cfg - m.mu.Unlock() - return nil -} -func (m *Module) Status(context.Context) (json.RawMessage, error) { - m.mu.RLock() - defer m.mu.RUnlock() - return json.Marshal(m.config) -} -func (m *Module) Invoke(ctx context.Context, in extensionv1.Invocation) (extensionv1.Result, error) { - if err := ctx.Err(); err != nil { - return extensionv1.Result{}, err - } - if in.Capability == extensionv1.CapabilityAdmin && in.Operation == "image.describe" { - raw, err := m.Status(ctx) - return extensionv1.Result{Payload: raw}, err - } - if in.Capability != extensionv1.CapabilityRequest { - return extensionv1.Result{}, errors.New("unsupported image capability") - } - m.mu.RLock() - config := m.config - m.mu.RUnlock() - var output any - switch in.Operation { - case "image.features": - output = config - case "image.native.validate": - var request extensionv1.ImageNativeRequest - if json.Unmarshal(in.Payload, &request) != nil { - return extensionv1.Result{}, errors.New("invalid native image facts") - } - if err := validateNativeImage(request); err != nil { - return extensionv1.Result{Code: "invalid_image_request", Message: err.Error(), HTTPStatus: 400}, nil - } - output = true - case "image.responses.plan": - var request extensionv1.ImageBridgeRequest - if json.Unmarshal(in.Payload, &request) != nil { - return extensionv1.Result{}, errors.New("invalid image bridge request") - } - plan, code, message := planResponsesBridge(request, config) - if code != "" { - return extensionv1.Result{Code: code, Message: message, HTTPStatus: 400}, nil - } - output = plan - case "image.studio.plan", "image.studio.validate": - var request extensionv1.ImageStudioPlanRequest - if json.Unmarshal(in.Payload, &request) != nil { - return extensionv1.Result{}, errors.New("invalid image plan request") - } - plan, code, message := planStudio(request) - if code != "" { - return extensionv1.Result{Code: code, Message: message, HTTPStatus: 400}, nil - } - if in.Operation == "image.studio.plan" && !config.StudioEnabled { - return extensionv1.Result{Code: "studio_disabled", Message: "Image Studio is not enabled", HTTPStatus: 404}, nil - } - output = plan - case "image.studio.models": - var request extensionv1.ImageStudioCatalogRequest - if json.Unmarshal(in.Payload, &request) != nil { - return extensionv1.Result{}, errors.New("invalid image capability request") - } - output = studioModels(request.Capabilities) - default: - return extensionv1.Result{}, errors.New("unsupported image operation") - } - raw, err := json.Marshal(output) - return extensionv1.Result{Payload: raw}, err -} - -func planStudio(in extensionv1.ImageStudioPlanRequest) (extensionv1.ImageStudioPlan, string, string) { - plan := extensionv1.ImageStudioPlan{Model: strings.TrimSpace(in.Model), Mode: in.Mode, Size: strings.TrimSpace(in.Size), Quality: strings.TrimSpace(in.Quality), OutputPerRequest: 1, ResponseFormat: "b64_json", Endpoint: "/v1/images/generations"} - if !in.APIKeySelected { - return plan, "invalid_api_key", "Select an Image Studio API key" - } - if in.PromptBytes <= 0 || in.PromptBytes > 32000 { - return plan, "invalid_prompt", "Prompt must contain between 1 and 32000 bytes" - } - if in.Count < 1 || in.Count > extensionv1.ImageStudioMaxOutputCount { - return plan, "invalid_count", "Image count must be between 1 and 4" - } - if plan.Model != extensionv1.ImageStudioModelGPTImage2 && plan.Model != extensionv1.ImageStudioModelGeminiProImage { - return plan, "unsupported_model", "Image Studio model is not supported" - } - if in.Mode != "generate" && in.Mode != "edit" { - return plan, "unsupported_mode", "Image Studio mode is not supported" - } - if in.Mode == "edit" && plan.Model != extensionv1.ImageStudioModelGeminiProImage { - return plan, "unsupported_mode", "This model does not support image editing" - } - if in.Mode == "edit" && !in.HasReference { - return plan, "reference_required", "A reference image is required for editing" - } - if in.Mode == "generate" && in.HasReference { - return plan, "reference_not_allowed", "Reference images are only supported for editing" - } - if in.HasMask && (in.Mode != "edit" || plan.Model != extensionv1.ImageStudioModelGeminiProImage) { - return plan, "mask_not_allowed", "Masks are not supported for this request" - } - if plan.Size != "" && plan.Size != "1024x1024" { - return plan, "unsupported_size", "Image size is not supported" - } - if plan.Quality != "" && plan.Quality != "low" { - return plan, "unsupported_quality", "Image quality is not supported" - } - if plan.Size == "" { - plan.Size = "1024x1024" - } - if plan.Quality == "" { - plan.Quality = "low" - } - if in.Mode == "edit" { - plan.Endpoint = "/v1/images/edits" - } - return plan, "", "" -} - -func studioModels(capabilities []extensionv1.CindyModelCapability) []extensionv1.CindyModelCapability { - result := make([]extensionv1.CindyModelCapability, 0, 2) - for _, model := range []string{extensionv1.ImageStudioModelGPTImage2, extensionv1.ImageStudioModelGeminiProImage} { - for _, capability := range capabilities { - if capability.ID != model || capability.Kind != extensionv1.CindyModelKindImage { - continue - } - if capability.Controls != nil { - capability.Controls = &extensionv1.CindyCapabilityControls{Generation: extensionv1.CloneCindyImageRequestControls(capability.Controls.Generation), Edit: extensionv1.CloneCindyImageRequestControls(capability.Controls.Edit)} - if capability.Controls.Generation != nil { - capability.Controls.Generation.MaxOutputCount = extensionv1.ImageStudioMaxOutputCount - } - if capability.Controls.Edit != nil { - capability.Controls.Edit.MaxOutputCount = extensionv1.ImageStudioMaxOutputCount - } - } - result = append(result, capability) - break - } - } - return result -} diff --git a/plugins/image-tools/policy/module_test.go b/plugins/image-tools/policy/module_test.go deleted file mode 100644 index e58a48b7a9e0..000000000000 --- a/plugins/image-tools/policy/module_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package policy - -import ( - "context" - "encoding/json" - "testing" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -func TestImageExecutionRequiresExplicitEnablementAndKeepsNativeOne(t *testing.T) { - module := New() - request := extensionv1.ImageStudioPlanRequest{APIKeySelected: true, Mode: "generate", Model: "gpt-image-2", PromptBytes: 12, Count: 4} - raw, _ := json.Marshal(request) - invocation := extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "image.studio.plan", Payload: raw} - out, err := module.Invoke(context.Background(), invocation) - if err != nil || out.Code != "studio_disabled" { - t.Fatalf("default must remain disabled: %+v %v", out, err) - } - if err := module.ApplyConfig(context.Background(), []byte(`{"studio_enabled":true}`)); err != nil { - t.Fatal(err) - } - out, err = module.Invoke(context.Background(), invocation) - if err != nil || out.Code != "" { - t.Fatalf("valid plan rejected: %+v %v", out, err) - } - var plan extensionv1.ImageStudioPlan - if json.Unmarshal(out.Payload, &plan) != nil || plan.OutputPerRequest != 1 || plan.Size != "1024x1024" || plan.Endpoint != "/v1/images/generations" { - t.Fatalf("unexpected plan: %s", out.Payload) - } - request.Mode, request.HasReference = "edit", true - invocation.Payload, _ = json.Marshal(request) - out, err = module.Invoke(context.Background(), invocation) - if err != nil || out.Code != "unsupported_mode" { - t.Fatalf("unsupported editing must be rejected: %+v %v", out, err) - } - request.Model = "gemini-3-pro-image" - invocation.Payload, _ = json.Marshal(request) - out, err = module.Invoke(context.Background(), invocation) - if err != nil || out.Code != "" || json.Unmarshal(out.Payload, &plan) != nil || plan.Endpoint != "/v1/images/edits" { - t.Fatalf("supported editing plan unavailable: %+v %v", out, err) - } -} diff --git a/plugins/image-tools/policy/native.go b/plugins/image-tools/policy/native.go deleted file mode 100644 index f55d568b1d4f..000000000000 --- a/plugins/image-tools/policy/native.go +++ /dev/null @@ -1,50 +0,0 @@ -package policy - -import ( - "fmt" - "strings" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -func validateNativeImage(in extensionv1.ImageNativeRequest) error { - capability := in.Capability - if !capability.PublicModel || capability.Kind != extensionv1.CindyModelKindImage || capability.Controls == nil { - return fmt.Errorf("model %q has no verified Cindy image capability", in.Model) - } - endpoint := extensionv1.CindyEndpointImagesGenerate - controls := capability.Controls.Generation - if in.Editing { - endpoint, controls = extensionv1.CindyEndpointImagesEdit, capability.Controls.Edit - } - if !in.Verified || controls == nil { - return fmt.Errorf("model %q is not verified for %s", capability.PublicID, endpoint) - } - if in.Stream { - return fmt.Errorf("stream is not verified for model %q on %s", capability.PublicID, endpoint) - } - if controls.MaxOutputCount <= 0 || in.Count <= 0 || in.Count > controls.MaxOutputCount { - return fmt.Errorf("n must be between 1 and %d for model %q on %s", controls.MaxOutputCount, capability.PublicID, endpoint) - } - if !imageControlAllows(controls.Sizes, in.Size) { - return fmt.Errorf("size %q is not verified for model %q on %s", in.Size, capability.PublicID, endpoint) - } - if !imageControlAllows(controls.Qualities, in.Quality) { - return fmt.Errorf("quality %q is not verified for model %q on %s", in.Quality, capability.PublicID, endpoint) - } - if in.ResponseFormat != "" && !strings.EqualFold(in.ResponseFormat, "b64_json") { - return fmt.Errorf("response_format must be b64_json for model %q", capability.PublicID) - } - if in.UnverifiedControls { - return fmt.Errorf("request contains an unverified image control for model %q on %s", capability.PublicID, endpoint) - } - if in.Editing { - if !controls.SupportsReferenceImage || !in.HasReference { - return fmt.Errorf("a reference image is required for model %q on %s", capability.PublicID, endpoint) - } - if in.HasMask && !controls.SupportsMask { - return fmt.Errorf("mask is not verified for model %q on %s", capability.PublicID, endpoint) - } - } - return nil -} diff --git a/plugins/image-tools/policy/native_test.go b/plugins/image-tools/policy/native_test.go deleted file mode 100644 index 734120aeeef1..000000000000 --- a/plugins/image-tools/policy/native_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package policy - -import ( - "testing" - - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -func TestNativeImageControlsKeepGenerationAndEditingIndependent(t *testing.T) { - request := extensionv1.ImageNativeRequest{Model: "fixture-image", Verified: true, Count: 1, Size: "1024x1024", Quality: "low", ResponseFormat: "b64_json", Capability: extensionv1.CindyCapability{PublicID: "fixture-image", PublicModel: true, Kind: extensionv1.CindyModelKindImage, Controls: &extensionv1.CindyCapabilityControls{Generation: &extensionv1.CindyImageRequestControls{Sizes: []string{"1024x1024"}, Qualities: []string{"low"}, MaxOutputCount: 1}}}} - if err := validateNativeImage(request); err != nil { - t.Fatal(err) - } - for _, change := range []func(*extensionv1.ImageNativeRequest){ - func(r *extensionv1.ImageNativeRequest) { r.Editing = true; r.HasReference = true }, - func(r *extensionv1.ImageNativeRequest) { r.Count = 2 }, - func(r *extensionv1.ImageNativeRequest) { r.Stream = true }, - func(r *extensionv1.ImageNativeRequest) { r.UnverifiedControls = true }, - func(r *extensionv1.ImageNativeRequest) { r.Verified = false }, - } { - copy := request - change(©) - if err := validateNativeImage(copy); err == nil { - t.Fatalf("unverified request admitted: %+v", copy) - } - } - request.Editing = true - request.Capability.Controls.Edit = &extensionv1.CindyImageRequestControls{Sizes: []string{"1024x1024"}, Qualities: []string{"low"}, MaxOutputCount: 1, SupportsReferenceImage: true} - if err := validateNativeImage(request); err == nil { - t.Fatal("missing reference admitted") - } - request.HasReference = true - if err := validateNativeImage(request); err != nil { - t.Fatal(err) - } - request.HasMask = true - if err := validateNativeImage(request); err == nil { - t.Fatal("unverified mask admitted") - } -} diff --git a/plugins/image-tools/scripts/check-ui.py b/plugins/image-tools/scripts/check-ui.py deleted file mode 100644 index 5849b77642b0..000000000000 --- a/plugins/image-tools/scripts/check-ui.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Offline image UI checks using native Blob transport and synthetic jobs.""" -import argparse -import json -import mimetypes -import threading -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from urllib.parse import urlsplit -from playwright.sync_api import expect, sync_playwright - -ROOT = Path(__file__).resolve().parents[3] -COMPILED = ROOT / 'plugins/image-tools/ui/compiled' -HARNESS = r'''''' - - -class Handler(BaseHTTPRequestHandler): - def log_message(self, *_): - pass - - def do_GET(self): - target = urlsplit(self.path).path - if target == '/': - data, mime = HARNESS.encode(), 'text/html; charset=utf-8' - else: - assets = {'/ui/' + f.relative_to(COMPILED).as_posix(): f for f in COMPILED.rglob('*') if f.is_file()} - if target not in assets: - self.send_error(404) - return - data, mime = assets[target].read_bytes(), mimetypes.guess_type(target)[0] or 'application/octet-stream' - self.send_response(200) - self.send_header('Content-Type', mime) - self.send_header('Cache-Control', 'no-store') - if target.startswith('/ui/'): - self.send_header('Access-Control-Allow-Origin', '*') - self.send_header('Cross-Origin-Resource-Policy', 'cross-origin') - self.send_header('Content-Security-Policy', "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'") - self.end_headers() - self.wfile.write(data) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--output', type=Path, required=True) - args = parser.parse_args() - args.output.mkdir(parents=True, exist_ok=True) - server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) - threading.Thread(target=server.serve_forever, daemon=True).start() - cases = [] - try: - with sync_playwright() as playwright: - browser = playwright.chromium.launch() - try: - for width in (1200, 390): - for theme in ('light', 'dark'): - page = browser.new_page(viewport={'width': width, 'height': 960}) - errors, external = [], [] - page.on('pageerror', lambda error: errors.append(str(error))) - def route_request(route): - if urlsplit(route.request.url).hostname != '127.0.0.1': - external.append(route.request.url) - route.abort() - else: - route.continue_() - page.route('**/*', route_request) - page.goto(f'http://127.0.0.1:{server.server_port}/?theme={theme}') - frame = page.frame_locator('iframe') - expect(frame.locator('[data-testid="api-key-select"]')).to_be_visible() - labels = frame.get_by_role('button').all_text_contents() - assert labels - frame.locator('[data-testid="api-key-select"]').select_option('9') - frame.locator('[data-testid="prompt-input"]').fill('离线灯塔图片') - frame.locator('[data-testid="submit-image"]').click() - expect(frame.locator('[data-testid="history-grid"] img')).to_be_visible() - page.wait_for_function('ownerCount(42)===1') - assert page.evaluate("created[0].api_key_id==='9' && !('api_key' in created[0])") - page.evaluate('changeContext({available:false})') - expect(frame.locator('[data-testid="submit-image"]')).to_be_disabled() - expect(frame.locator('[data-testid="history-grid"] img')).to_be_visible() - expect(frame.locator('[data-testid="clear-history"]')).to_be_enabled() - assert frame.locator('body').evaluate('node=>node.scrollWidth<=innerWidth') - page.screenshot(path=str(args.output / f'image-{width}-{theme}.png'), full_page=True) - page.evaluate('changeContext({actor_id:88,available:true})') - expect(frame.locator('[data-testid="empty-history"]')).to_be_visible() - assert page.evaluate('ownerCount(42)') == 1 - assert not errors, errors - assert not external, external - cases.append({'width': width, 'theme': theme, 'blob_roundtrip': True, 'owner_isolated': True, 'page_errors': errors, 'external_requests': 0}) - page.close() - finally: - browser.close() - finally: - server.shutdown() - server.server_close() - result = {'offline_only': True, 'cases': cases} - (args.output / 'image-ui-check.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') - print(json.dumps(result, ensure_ascii=False)) - - -if __name__ == '__main__': - main() diff --git a/plugins/image-tools/ui/assets/app.js b/plugins/image-tools/ui/assets/app.js deleted file mode 100644 index 9d3fc4a8bd7e..000000000000 --- a/plugins/image-tools/ui/assets/app.js +++ /dev/null @@ -1,33 +0,0 @@ -/* global Sub2APIPluginBridge */ -(async () => { - const bridge = new Sub2APIPluginBridge() - window.addEventListener('pagehide', () => bridge.dispose(), { once: true }) - const context = await bridge.context() - document.documentElement.dataset.theme = context.theme || 'light' - const en = String(context.locale).startsWith('en') - const text = (zh, english) => en ? english : zh - const node = (tag, value = '') => { const element = document.createElement(tag); element.textContent = value; return element } - const app = document.getElementById('app') - new ResizeObserver(() => bridge.resize(document.documentElement.scrollHeight)).observe(app) - app.append(node('h2', text('图像工具', 'Image tools'))) - app.append(node('p', text('Image Studio 使用账号现有的图像能力与分组权限。每次上游请求生成一张图像,一项任务最多四张。关闭功能会停止新的生成操作,已保存的结果保留。', 'Image Studio uses existing account image capabilities and group permissions. Each upstream request generates one image, with up to four images per job. Disabling the feature stops new generation work and preserves saved results.'))) - let config = await bridge.config() - const fields = [['studio_enabled', '启用 Image Studio', 'Enable Image Studio'], ['responses_image_enabled', '启用 Cindy Responses 图像桥接', 'Enable Cindy Responses image bridge']] - const controls = fields.map(([key, zh, english]) => { - const input = node('input'); input.type = 'checkbox'; input.checked = !!config[key] - const label = node('label'); label.append(input, document.createTextNode(text(zh, english))); app.append(label) - return { key, input } - }) - const status = node('p'); status.setAttribute('role', 'status') - const save = node('button', text('保存设置', 'Save settings')); save.type = 'button' - save.onclick = async () => { - save.disabled = true - try { - config = await bridge.save(Object.fromEntries(controls.map(({ key, input }) => [key, input.checked]))) - controls.forEach(({ key, input }) => { input.checked = !!config[key] }) - status.textContent = text('设置已保存', 'Settings saved') - } catch (error) { status.textContent = error.message || text('保存失败', 'Save failed') } - finally { save.disabled = false } - } - app.append(save, status) -})().catch(error => { document.getElementById('app').textContent = error.message || 'Image tools unavailable' }) diff --git a/plugins/image-tools/ui/assets/styles.css b/plugins/image-tools/ui/assets/styles.css deleted file mode 100644 index 8f0f57219d89..000000000000 --- a/plugins/image-tools/ui/assets/styles.css +++ /dev/null @@ -1,11 +0,0 @@ -:root { color-scheme:light; --bg:#fff; --fg:#1f2937; --muted:#6b7280; --line:#d1d5db; } -:root[data-theme=dark] { color-scheme:dark; --bg:#111827; --fg:#e5e7eb; --muted:#9ca3af; --line:#374151; } -* { box-sizing:border-box; } -body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.6 system-ui,sans-serif; } -main { padding:20px; max-width:920px; margin:auto; overflow-wrap:anywhere; } -h2 { margin:0 0 16px; font-size:18px; } -p { margin:8px 0; color:var(--muted); } -label { display:flex; align-items:center; gap:10px; padding:8px 0; } -button { color:inherit; background:transparent; border:1px solid var(--line); border-radius:6px; padding:8px 14px; margin:12px 0; cursor:pointer; font:inherit; } -button:disabled { opacity:.55; cursor:wait; } -@media(max-width:480px) { main { padding:12px; } } diff --git a/plugins/image-tools/ui/index.html b/plugins/image-tools/ui/index.html deleted file mode 100644 index abd635c190d2..000000000000 --- a/plugins/image-tools/ui/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -图像工具 -
diff --git a/plugins/image-tools/ui/src/history.ts b/plugins/image-tools/ui/src/history.ts deleted file mode 100644 index 660146030f6f..000000000000 --- a/plugins/image-tools/ui/src/history.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { resource } from '@sub2api/plugin-ui' -import type { ImageStudioHistoryRecord } from '@sub2api/plugin-ui/media' -export type { ImageStudioMode, ImageStudioHistoryRecord } from '@sub2api/plugin-ui/media' - -// Owner arguments preserve the page's late-result checks. The host derives the -// actual storage owner from its authenticated actor, never from these inputs. -export const listImageStudioHistory = (_owner: string) => resource('image.history.list') -export const saveImageStudioHistory = (_owner: string, record: ImageStudioHistoryRecord) => resource('image.history.save', { local_data: record }) -export const deleteImageStudioHistory = (_owner: string, id: string) => resource('image.history.delete', { local_data: id }) -export const clearImageStudioHistory = (_owner: string) => resource('image.history.clear') diff --git a/plugins/image-tools/ui/src/index.html b/plugins/image-tools/ui/src/index.html deleted file mode 100644 index b72f35353499..000000000000 --- a/plugins/image-tools/ui/src/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Image Studio
diff --git a/plugins/image-tools/ui/src/main.ts b/plugins/image-tools/ui/src/main.ts deleted file mode 100644 index cc47efff8902..000000000000 --- a/plugins/image-tools/ui/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mountPlugin } from '@sub2api/plugin-ui' -import '@sub2api/plugin-ui/base.css' -import App from './App.vue' -import zh from './locales/zh' -import en from './locales/en' -void mountPlugin(App, { zh, en }).catch(error => { document.getElementById('app')!.textContent = error instanceof Error ? error.message : 'Plugin unavailable' }) From 6d49c712ff852211fb54aaaed41e529ac23e6b31 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 11:00:20 +0800 Subject: [PATCH 03/13] refactor(deplugin): fold admin-observability into core settings and restore upstream Prompt Audit Account traffic telemetry no longer asks the admin-observability plugin for its policy. The telemetry and flat theme switches live in the admin_observability_config setting (default: the legacy gateway flag, theme on), load at startup and are edited through a native "Appearance and observations" panel (GET/PUT /api/v1/admin/settings/observability). The outcome rules are a static in-process table. The plugin's account-details traffic view was never rendered by the host, so no page replaces it; the existing GET /admin/accounts/:id/traffic-telemetry route stays. The flat theme and the MiSans fonts are bundled natively again (frontend/src/styles/flat-theme.css, frontend/src/assets/fonts/misans). The theme applies while carries the flat-theme class, which follows the new public flat_theme_enabled setting and is set before mount from the injected config, so there is no flash. Upstream v0.2.8 ships Prompt Audit as a native page. Its files are restored (api.ts, types, EventDetailDialog, locales, route registration, sidebar entry) and only the improvements made while it lived in the plugin are kept: request generations and unmount guards, reconcileSavedDraft, the awaited delete confirmation, flat-theme class tweaks and a sticky save bar. The fork-only /admin/prompt-audit/groups route and group_plugin_options.go are removed; the page lists groups through /admin/groups/all as upstream does. The plugin module, bundle entry, go.mod replace, process contract test and the manifest fixture test are removed. A small JSON setting helper is shared with the image tools setting. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/test_first_party_plugins.sh | 3 +- backend/go.mod | 3 - .../account_handler_traffic_telemetry_test.go | 39 +- .../handler/admin/group_plugin_options.go | 32 -- .../internal/handler/admin/setting_handler.go | 26 +- backend/internal/handler/dto/settings.go | 1 + backend/internal/handler/setting_handler.go | 1 + backend/internal/server/routes/admin.go | 31 +- .../service/account_traffic_observe.go | 26 +- .../internal/service/admin_observability.go | 74 +++ .../service/admin_observability_test.go | 116 +++++ .../service/plugin_extension_runtime_test.go | 30 -- .../service/plugin_manifest_schema_test.go | 21 - .../internal/service/plugin_observability.go | 47 -- .../service/plugin_observability_test.go | 155 ------ .../service/plugin_prompt_fixture_test.go | 4 - .../service/setting_admin_observability.go | 42 ++ .../internal/service/setting_image_tools.go | 37 +- backend/internal/service/setting_json.go | 37 ++ backend/internal/service/setting_public.go | 3 + backend/internal/service/settings_view.go | 1 + backend/internal/service/wire.go | 4 +- backend/internal/testextensions/fixtures.go | 5 +- frontend/public/fonts/misans/NOTICE.md | 2 +- frontend/src/api/admin/settings.ts | 18 + .../assets/fonts/misans}/MiSans-Bold.woff2 | Bin .../assets/fonts/misans}/MiSans-Medium.woff2 | Bin .../assets/fonts/misans}/MiSans-Regular.woff2 | Bin .../fonts/misans}/MiSans-Semibold.woff2 | Bin .../admin/ObservabilitySettingsPanel.vue | 63 +++ frontend/src/components/layout/AppSidebar.vue | 2 +- .../StripePaymentInline.contrast.spec.ts | 2 +- .../features/prompt-audit/PromptAuditView.vue | 480 +++++++++++++++++- .../__tests__/PromptAuditView.spec.ts | 2 +- .../prompt-audit/__tests__/api.spec.ts | 44 ++ .../__tests__/components.spec.ts | 4 +- .../__tests__/integrationSurface.spec.ts | 8 +- .../prompt-audit}/__tests__/viewModel.spec.ts | 0 frontend/src/features/prompt-audit/api.ts | 120 +++++ .../prompt-audit}/components/EndpointPool.vue | 3 +- .../components/EventDetailDialog.vue | 2 +- .../components/EventWorkspace.vue | 2 +- .../components/FilterDeleteDialog.vue | 2 +- .../prompt-audit}/components/PolicyPanel.vue | 0 .../components/RuntimeOverview.vue | 0 .../src/features/prompt-audit}/types.ts | 0 .../src/features/prompt-audit}/viewModel.ts | 0 .../src/i18n/locales/en/admin/promptAudit.ts | 102 +++- .../src/i18n/locales/en/admin/settings.ts | 10 + .../src/i18n/locales/zh/admin/promptAudit.ts | 102 +++- .../src/i18n/locales/zh/admin/settings.ts | 10 + frontend/src/main.ts | 3 + frontend/src/stores/app.ts | 2 + .../src/styles/flat-theme.css | 50 +- frontend/src/types/index.ts | 2 + frontend/src/utils/flatTheme.ts | 6 + frontend/src/views/admin/SettingsView.vue | 2 + .../admin-observability/cmd/plugin/main.go | 12 - plugins/admin-observability/go.mod | 24 - plugins/admin-observability/go.sum | 83 --- .../admin-observability/manifest.source.json | 157 ------ plugins/admin-observability/policy/module.go | 134 ----- .../admin-observability/policy/module_test.go | 34 -- .../scripts/check-chart-colors.py | 62 --- .../scripts/check-theme.py | 84 --- .../admin-observability/scripts/check-ui.py | 112 ---- plugins/admin-observability/ui/assets/app.js | 58 --- ...ans-Font-Intellectual-Property-License.pdf | Bin 79535 -> 0 bytes .../ui/assets/fonts/NOTICE.md | 14 - .../admin-observability/ui/assets/styles.css | 16 - plugins/admin-observability/ui/index.html | 3 - .../ui/src/PromptAuditView.vue | 480 ------------------ .../ui/src/__tests__/api.spec.ts | 23 - plugins/admin-observability/ui/src/api.ts | 23 - .../ui/src/components/Pagination.vue | 14 - plugins/admin-observability/ui/src/index.html | 2 - .../admin-observability/ui/src/locales/en.ts | 288 ----------- .../admin-observability/ui/src/locales/zh.ts | 288 ----------- plugins/admin-observability/ui/src/main.ts | 6 - plugins/bundle.source.json | 3 +- 80 files changed, 1338 insertions(+), 2363 deletions(-) delete mode 100644 backend/internal/handler/admin/group_plugin_options.go create mode 100644 backend/internal/service/admin_observability.go create mode 100644 backend/internal/service/admin_observability_test.go delete mode 100644 backend/internal/service/plugin_observability.go create mode 100644 backend/internal/service/setting_admin_observability.go create mode 100644 backend/internal/service/setting_json.go rename {plugins/admin-observability/ui/assets/fonts => frontend/src/assets/fonts/misans}/MiSans-Bold.woff2 (100%) rename {plugins/admin-observability/ui/assets/fonts => frontend/src/assets/fonts/misans}/MiSans-Medium.woff2 (100%) rename {plugins/admin-observability/ui/assets/fonts => frontend/src/assets/fonts/misans}/MiSans-Regular.woff2 (100%) rename {plugins/admin-observability/ui/assets/fonts => frontend/src/assets/fonts/misans}/MiSans-Semibold.woff2 (100%) create mode 100644 frontend/src/components/admin/ObservabilitySettingsPanel.vue rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/__tests__/PromptAuditView.spec.ts (99%) create mode 100644 frontend/src/features/prompt-audit/__tests__/api.spec.ts rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/__tests__/components.spec.ts (99%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/__tests__/viewModel.spec.ts (100%) create mode 100644 frontend/src/features/prompt-audit/api.ts rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/components/EndpointPool.vue (99%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/components/EventDetailDialog.vue (99%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/components/EventWorkspace.vue (99%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/components/FilterDeleteDialog.vue (99%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/components/PolicyPanel.vue (100%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/components/RuntimeOverview.vue (100%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/types.ts (100%) rename {plugins/admin-observability/ui/src => frontend/src/features/prompt-audit}/viewModel.ts (100%) rename plugins/admin-observability/ui/assets/theme.css => frontend/src/styles/flat-theme.css (84%) create mode 100644 frontend/src/utils/flatTheme.ts delete mode 100644 plugins/admin-observability/cmd/plugin/main.go delete mode 100644 plugins/admin-observability/go.mod delete mode 100644 plugins/admin-observability/go.sum delete mode 100644 plugins/admin-observability/manifest.source.json delete mode 100644 plugins/admin-observability/policy/module.go delete mode 100644 plugins/admin-observability/policy/module_test.go delete mode 100644 plugins/admin-observability/scripts/check-chart-colors.py delete mode 100644 plugins/admin-observability/scripts/check-theme.py delete mode 100644 plugins/admin-observability/scripts/check-ui.py delete mode 100644 plugins/admin-observability/ui/assets/app.js delete mode 100644 plugins/admin-observability/ui/assets/fonts/MiSans-Font-Intellectual-Property-License.pdf delete mode 100644 plugins/admin-observability/ui/assets/fonts/NOTICE.md delete mode 100644 plugins/admin-observability/ui/assets/styles.css delete mode 100644 plugins/admin-observability/ui/index.html delete mode 100644 plugins/admin-observability/ui/src/PromptAuditView.vue delete mode 100644 plugins/admin-observability/ui/src/__tests__/api.spec.ts delete mode 100644 plugins/admin-observability/ui/src/api.ts delete mode 100644 plugins/admin-observability/ui/src/components/Pagination.vue delete mode 100644 plugins/admin-observability/ui/src/index.html delete mode 100644 plugins/admin-observability/ui/src/locales/en.ts delete mode 100644 plugins/admin-observability/ui/src/locales/zh.ts delete mode 100644 plugins/admin-observability/ui/src/main.ts diff --git a/.github/scripts/test_first_party_plugins.sh b/.github/scripts/test_first_party_plugins.sh index dc50934ad1c2..33735d1d60d9 100644 --- a/.github/scripts/test_first_party_plugins.sh +++ b/.github/scripts/test_first_party_plugins.sh @@ -32,7 +32,6 @@ export SUB2API_EXTENSION_TEST_BUNDLE="$plugin_build_dir/bundle/lock.json" export SUB2API_PROMPT_SKILLS_TEST_BINARY="$plugin_build_dir/binaries/prompt-skills" export SUB2API_ACCOUNT_TOOLS_TEST_BINARY="$plugin_build_dir/binaries/account-tools" export SUB2API_CINDY_PROVIDER_TEST_BINARY="$plugin_build_dir/binaries/cindy-provider" -export SUB2API_ADMIN_OBSERVABILITY_TEST_BINARY="$plugin_build_dir/binaries/admin-observability" export SUB2API_EXTENSION_TEST_PUBLIC_KEY SUB2API_EXTENSION_TEST_PUBLIC_KEY=$(jq -r .public_key "$plugin_build_dir/publisher.json") -go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestObservabilityExtensionRuntimeUsesScopedMetricsBroker|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 +go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 diff --git a/backend/go.mod b/backend/go.mod index 165c18228974..6d77f1c14759 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,14 +10,11 @@ replace github.com/HTExplicit/sub2api-plugins/accounttools => ../plugins/account replace github.com/HTExplicit/sub2api-plugins/cindyprovider => ../plugins/cindy-provider -replace github.com/HTExplicit/sub2api-plugins/adminobservability => ../plugins/admin-observability - require ( github.com/HTExplicit/sub2api-plugins/codexruntime v0.0.0 github.com/HTExplicit/sub2api-plugins/promptskills v0.0.0 github.com/HTExplicit/sub2api-plugins/accounttools v0.0.0 github.com/HTExplicit/sub2api-plugins/cindyprovider v0.0.0 - github.com/HTExplicit/sub2api-plugins/adminobservability v0.0.0 entgo.io/ent v0.14.5 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/alibabacloud-go/captcha-20230305 v1.1.3 diff --git a/backend/internal/handler/admin/account_handler_traffic_telemetry_test.go b/backend/internal/handler/admin/account_handler_traffic_telemetry_test.go index 4c5c88b99437..6ce1663d1c07 100644 --- a/backend/internal/handler/admin/account_handler_traffic_telemetry_test.go +++ b/backend/internal/handler/admin/account_handler_traffic_telemetry_test.go @@ -43,34 +43,12 @@ func (s *scopedTrafficAdminService) GetAccount(ctx context.Context, id int64) (* return s.stubAdminService.GetAccount(ctx, id) } -type scopedTrafficPolicy struct { - t *testing.T - allowed bool - calls int -} - -func (p *scopedTrafficPolicy) InvokeOperation(_ context.Context, platform, kind string, in extensionv1.Invocation) (extensionv1.Result, error) { - p.calls++ - require.Equal(p.t, service.PlatformOpenAI, platform) - require.Equal(p.t, service.AccountTypeAPIKey, kind) - require.EqualValues(p.t, 18, in.AccountID) - require.Equal(p.t, extensionv1.CapabilityObservability, in.Capability) - require.Equal(p.t, "observability.policy", in.Operation) - require.JSONEq(p.t, `{}`, string(in.Payload)) - if !p.allowed { - return extensionv1.Result{}, service.ErrExtensionOperationDisabled - } - raw, err := json.Marshal(extensionv1.TrafficObservationPolicy{Enabled: true, Classification: extensionv1.DecisionTable{Default: string(service.AccountTrafficOutcomeFailedOther)}}) - return extensionv1.Result{Payload: raw}, err -} - -func TestAccountHandlerTrafficTelemetryUsesLoadedAccountScope(t *testing.T) { +func TestAccountHandlerTrafficTelemetryFollowsTelemetrySwitch(t *testing.T) { gin.SetMode(gin.TestMode) - for _, allowed := range []bool{false, true} { - t.Run(strconv.FormatBool(allowed), func(t *testing.T) { + for _, enabled := range []bool{false, true} { + t.Run(strconv.FormatBool(enabled), func(t *testing.T) { t.Cleanup(testextensions.Install) - policy := &scopedTrafficPolicy{t: t, allowed: allowed} - service.ConfigureProcessExtensionServices(nil, policy) + service.ConfigureAdminObservability(&extensionv1.AdminObservabilityConfig{TelemetryEnabled: enabled}) stub := &scopedTrafficAdminService{stubAdminService: newStubAdminService()} stub.getAccountResult = &service.Account{ID: 18, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey, Concurrency: 3, Credentials: map[string]any{"api_key": "never-return-secret"}} cache := &stubAccountTrafficObserveCache{snapshot: map[service.AccountTrafficProtocol]service.AccountTrafficObserveState{service.AccountTrafficProtocolHTTP: {Started: 3, Completed2xx: 2}}} @@ -86,18 +64,17 @@ func TestAccountHandlerTrafficTelemetryUsesLoadedAccountScope(t *testing.T) { Data map[string]any `json:"data"` } require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response)) - require.Equal(t, allowed, response.Data["state_available"]) + require.Equal(t, enabled, response.Data["state_available"]) require.EqualValues(t, 18, response.Data["account_id"]) - require.Equal(t, 1, stub.reads, "policy selection must reuse the account already loaded by the handler") - require.Equal(t, 1, policy.calls, "an unscoped Enabled preflight must not precede account-specific admission") + require.Equal(t, 1, stub.reads) require.NotContains(t, recorder.Body.String(), "never-return-secret") - if allowed { + if enabled { require.Equal(t, 1, cache.snapshotCalls) require.Contains(t, response.Data, "protocols") } else { require.Zero(t, cache.snapshotCalls) require.NotContains(t, response.Data, "protocols") - require.NotContains(t, response.Data, "error", "out-of-scope telemetry keeps the existing disabled no-data response") + require.NotContains(t, response.Data, "error", "disabled telemetry keeps the no-data response") } }) } diff --git a/backend/internal/handler/admin/group_plugin_options.go b/backend/internal/handler/admin/group_plugin_options.go deleted file mode 100644 index ab1d4b1785d2..000000000000 --- a/backend/internal/handler/admin/group_plugin_options.go +++ /dev/null @@ -1,32 +0,0 @@ -package admin - -import ( - "github.com/Wei-Shaw/sub2api/internal/pkg/response" - "github.com/Wei-Shaw/sub2api/internal/service" - "github.com/gin-gonic/gin" -) - -// PluginOptions supplies only the labels and identifiers needed by a group -// picker. It preserves the host's simple-mode restrictions and omits settings. -func (h *GroupHandler) PluginOptions(c *gin.Context) { - if h == nil || h.adminService == nil { - response.Error(c, 503, "Group options unavailable") - return - } - if h.rejectUnsupportedSimpleModeOperation(c, simpleModeGroupGetAll) { - return - } - groups, err := h.adminService.GetAllGroupsIncludingInactive(c.Request.Context()) - if err != nil { - response.ErrorFrom(c, err) - return - } - options := make([]gin.H, 0, len(groups)) - for _, group := range groups { - if h.isSimpleMode() && !service.IsGroupBindableInSimpleMode(&group) { - continue - } - options = append(options, gin.H{"id": group.ID, "name": group.Name, "platform": group.Platform, "status": group.Status}) - } - response.Success(c, options) -} diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index dc4c0e0c7cda..1d42096550f0 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -100,8 +100,6 @@ func (h *SettingHandler) SetStepUpDeps(totpService *service.TotpService, userSer h.userService = userService } -// GetSettings 获取所有系统设置 -// GET /api/v1/admin/settings // GetOfficialModelContextCatalog returns the release-pinned official model // capacity catalog for the read-only admin browser. // GET /api/v1/admin/settings/model-context-catalog @@ -133,6 +131,30 @@ func (h *SettingHandler) UpdateImageToolsSettings(c *gin.Context) { response.Success(c, req) } +// GetObservabilitySettings returns the account traffic telemetry and flat theme switches. +// GET /api/v1/admin/settings/observability +func (h *SettingHandler) GetObservabilitySettings(c *gin.Context) { + response.Success(c, h.settingService.GetAdminObservabilityConfig(c.Request.Context())) +} + +// UpdateObservabilitySettings saves the account traffic telemetry and flat theme switches. +// Omitted switches stay on, as they did in the former plugin configuration. +// PUT /api/v1/admin/settings/observability +func (h *SettingHandler) UpdateObservabilitySettings(c *gin.Context) { + req := extensionv1.AdminObservabilityConfig{TelemetryEnabled: true, ThemeEnabled: true} + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid observability settings") + return + } + if err := h.settingService.UpdateAdminObservabilityConfig(c.Request.Context(), req); err != nil { + response.ErrorFrom(c, err) + return + } + response.Success(c, req) +} + +// GetSettings 获取所有系统设置 +// GET /api/v1/admin/settings func (h *SettingHandler) GetSettings(c *gin.Context) { c.Header("Cache-Control", "no-store") settings, err := h.settingService.GetAllSettings(c.Request.Context()) diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 3a0d7bfb94d0..d425c581b5c3 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -460,6 +460,7 @@ type PublicSettings struct { AvailableChannelsEnabled bool `json:"available_channels_enabled"` ImageStudioEnabled bool `json:"image_studio_enabled"` + FlatThemeEnabled bool `json:"flat_theme_enabled"` SubscriptionEnabled bool `json:"subscription_enabled"` diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go index 1189bba9eded..659d9103ac98 100644 --- a/backend/internal/handler/setting_handler.go +++ b/backend/internal/handler/setting_handler.go @@ -148,6 +148,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { AvailableChannelsEnabled: settings.AvailableChannelsEnabled, ImageStudioEnabled: settings.ImageStudioEnabled, + FlatThemeEnabled: settings.FlatThemeEnabled, SubscriptionEnabled: settings.SubscriptionEnabled, ModelPlazaEnabled: settings.ModelPlazaEnabled, diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 8dab25f47dd9..cb6a687ce11c 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -230,24 +230,17 @@ func registerSystemPromptRoutes(admin *gin.RouterGroup, h *handler.Handlers) { func registerPromptAuditRoutes(admin *gin.RouterGroup, h *handler.Handlers) { promptAudit := admin.Group("/prompt-audit") - for _, route := range []struct { - method, path, name string - handler gin.HandlerFunc - }{ - {"GET", "/config", "config", h.Admin.PromptAudit.GetConfig}, - {"PUT", "/config", "update", h.Admin.PromptAudit.UpdateConfig}, - {"POST", "/endpoints/probe", "probe", h.Admin.PromptAudit.ProbeEndpoint}, - {"GET", "/runtime", "runtime", h.Admin.PromptAudit.GetRuntime}, - {"GET", "/events", "events", h.Admin.PromptAudit.ListEvents}, - {"GET", "/events/:id", "event", h.Admin.PromptAudit.GetEvent}, - {"DELETE", "/events/:id", "delete", h.Admin.PromptAudit.DeleteEvent}, - {"POST", "/events/batch-delete", "batch-delete", h.Admin.PromptAudit.BatchDelete}, - {"POST", "/events/delete-preview", "delete-preview", h.Admin.PromptAudit.DeletePreview}, - {"POST", "/events/delete-by-filter", "delete-by-filter", h.Admin.PromptAudit.DeleteByFilter}, - {"GET", "/groups", "groups", h.Admin.Group.PluginOptions}, - } { - descriptor := extensionv1.ResourceDescriptor{ResourceGrant: extensionv1.ResourceGrant{Name: "prompt-audit." + route.name, Capability: extensionv1.CapabilityUI, Permission: "admin"}, Method: route.method, Path: promptAudit.BasePath() + route.path, AllAccounts: true} - promptAudit.Handle(route.method, route.path, h.Admin.Plugin.RegisterResource(descriptor), route.handler) + { + promptAudit.GET("/config", h.Admin.PromptAudit.GetConfig) + promptAudit.PUT("/config", h.Admin.PromptAudit.UpdateConfig) + promptAudit.POST("/endpoints/probe", h.Admin.PromptAudit.ProbeEndpoint) + promptAudit.GET("/runtime", h.Admin.PromptAudit.GetRuntime) + promptAudit.GET("/events", h.Admin.PromptAudit.ListEvents) + promptAudit.GET("/events/:id", h.Admin.PromptAudit.GetEvent) + promptAudit.DELETE("/events/:id", h.Admin.PromptAudit.DeleteEvent) + promptAudit.POST("/events/batch-delete", h.Admin.PromptAudit.BatchDelete) + promptAudit.POST("/events/delete-preview", h.Admin.PromptAudit.DeletePreview) + promptAudit.POST("/events/delete-by-filter", h.Admin.PromptAudit.DeleteByFilter) } } @@ -725,6 +718,8 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { adminSettings.GET("/model-context-catalog", h.Admin.Setting.GetOfficialModelContextCatalog) adminSettings.GET("/image-tools", h.Admin.Setting.GetImageToolsSettings) adminSettings.PUT("/image-tools", h.Admin.Setting.UpdateImageToolsSettings) + adminSettings.GET("/observability", h.Admin.Setting.GetObservabilitySettings) + adminSettings.PUT("/observability", h.Admin.Setting.UpdateObservabilitySettings) adminSettings.POST("/openai-codex-ticket/proxy-test", h.Admin.Setting.TestCodexTicketProxy) adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection) adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) diff --git a/backend/internal/service/account_traffic_observe.go b/backend/internal/service/account_traffic_observe.go index 2537353e34e0..a383c2f75ccd 100644 --- a/backend/internal/service/account_traffic_observe.go +++ b/backend/internal/service/account_traffic_observe.go @@ -119,22 +119,18 @@ type AccountTrafficObserver struct { cache AccountTrafficObserveCache } -// The legacy gateway flag is imported during plugin bootstrap. Runtime intent -// and classification come from the observability plugin. +// Whether turns are recorded follows the admin_observability_config setting, +// whose default is the legacy gateway flag. func NewAccountTrafficObserver(cache AccountTrafficObserveCache, _ *config.Config) *AccountTrafficObserver { return &AccountTrafficObserver{ cache: cache, } } -// Enabled reports unscoped plugin availability for management displays. It is -// not account admission: Begin and Snapshot resolve the actual account scope. +// Enabled reports whether telemetry is switched on, for management displays. +// Begin and Snapshot additionally require a concrete account identity. func (o *AccountTrafficObserver) Enabled() bool { - if o == nil || o.cache == nil { - return false - } - policy, ok := currentTrafficObservationPolicy(context.Background()) - return ok && policy.Enabled + return o != nil && o.cache != nil && currentAdminObservabilityConfig().TelemetryEnabled } // Begin records one started turn for account/protocol and samples in-flight @@ -147,7 +143,7 @@ func (o *AccountTrafficObserver) Begin(ctx context.Context, account *Account, pr if ctx == nil { ctx = context.Background() } - policy, available := currentAccountTrafficObservationPolicy(ctx, account) + policy, available := currentAccountTrafficObservationPolicy(account) if !available || !policy.Enabled { return nil } @@ -173,7 +169,7 @@ func (o *AccountTrafficObserver) Snapshot(ctx context.Context, account *Account) if ctx == nil { ctx = context.Background() } - policy, available := currentAccountTrafficObservationPolicy(ctx, account) + policy, available := currentAccountTrafficObservationPolicy(account) if !available || !policy.Enabled { return nil, ErrAccountTrafficTelemetryUnavailable } @@ -218,13 +214,9 @@ func (t *AccountTrafficTurn) Finish(result *OpenAIForwardResult, err error, clie } // Compatibility facade for callers without a begun turn. Started observations -// instead evaluate the immutable rule table captured before incrementing. +// instead evaluate the rule table captured before incrementing. func classifyAccountTrafficOutcome(result *OpenAIForwardResult, err error, clientCancelled bool) AccountTrafficOutcome { - policy, ok := currentTrafficObservationPolicy(context.Background()) - if !ok { - return AccountTrafficOutcomeFailedOther - } - return evaluateAccountTrafficOutcome(policy.Classification, result, err, clientCancelled) + return evaluateAccountTrafficOutcome(accountTrafficOutcomeRules, result, err, clientCancelled) } func evaluateAccountTrafficOutcome(table extensionv1.DecisionTable, result *OpenAIForwardResult, err error, clientCancelled bool) AccountTrafficOutcome { diff --git a/backend/internal/service/admin_observability.go b/backend/internal/service/admin_observability.go new file mode 100644 index 000000000000..e6c60a9dcc99 --- /dev/null +++ b/backend/internal/service/admin_observability.go @@ -0,0 +1,74 @@ +package service + +import ( + "sync/atomic" + + "github.com/Wei-Shaw/sub2api/internal/config" + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +var adminObservabilityConfigOverride atomic.Pointer[extensionv1.AdminObservabilityConfig] + +// LegacyAdminObservabilityConfig is the deploy-time default: account traffic +// telemetry follows the legacy gateway flag and the flat theme is on. +func LegacyAdminObservabilityConfig(cfg *config.Config) extensionv1.AdminObservabilityConfig { + return extensionv1.AdminObservabilityConfig{TelemetryEnabled: cfg == nil || !cfg.Gateway.AccountTrafficTelemetryDisabled, ThemeEnabled: true} +} + +// ConfigureAdminObservability installs the effective telemetry and theme +// switches (startup load, admin update, tests). A nil value restores the +// built-in default. +func ConfigureAdminObservability(config *extensionv1.AdminObservabilityConfig) { + adminObservabilityConfigOverride.Store(config) +} + +func currentAdminObservabilityConfig() extensionv1.AdminObservabilityConfig { + if config := adminObservabilityConfigOverride.Load(); config != nil { + return *config + } + return LegacyAdminObservabilityConfig(nil) +} + +// FlatThemeEnabled reports whether the flat site theme is switched on. +func FlatThemeEnabled() bool { + return currentAdminObservabilityConfig().ThemeEnabled +} + +// currentAccountTrafficObservationPolicy returns the telemetry switch and the +// outcome rules for one account. Accounts without a concrete identity are never +// observed. +func currentAccountTrafficObservationPolicy(account *Account) (extensionv1.TrafficObservationPolicy, bool) { + if account == nil || account.ID <= 0 || account.Platform == "" || account.Platform == "*" || account.Type == "" || account.Type == "*" { + return extensionv1.TrafficObservationPolicy{}, false + } + return extensionv1.TrafficObservationPolicy{Enabled: currentAdminObservabilityConfig().TelemetryEnabled, Classification: accountTrafficOutcomeRules}, true +} + +// accountTrafficOutcomeRules classifies a finished turn. Client cancellation +// outranks errors, and a WebSocket turn counts as completed only with a +// demonstrable terminal event. Every result is an AccountTrafficOutcome. +var accountTrafficOutcomeRules = func() extensionv1.DecisionTable { + eq := func(field, value string) extensionv1.DecisionCondition { + return extensionv1.DecisionCondition{Field: field, Operator: "eq", Value: value} + } + num := func(field, op, value string) extensionv1.DecisionCondition { + return extensionv1.DecisionCondition{Field: field, Operator: op, Value: value} + } + rule := func(result AccountTrafficOutcome, when ...extensionv1.DecisionCondition) extensionv1.DecisionRule { + return extensionv1.DecisionRule{When: when, Result: string(result)} + } + return extensionv1.DecisionTable{Default: string(AccountTrafficOutcomeFailedOther), Rules: []extensionv1.DecisionRule{ + rule(AccountTrafficOutcomeCancelled, eq("client_cancelled", "true")), + rule(AccountTrafficOutcomeCancelled, eq("ws", "true"), eq("terminal", "response.cancelled")), + rule(AccountTrafficOutcomeCancelled, eq("ws", "true"), eq("terminal", "response.incomplete")), + rule(AccountTrafficOutcomeUpstream429, eq("has_error", "true"), eq("error_status", "429")), + rule(AccountTrafficOutcomeUpstream5xx, eq("has_error", "true"), num("error_status", "gte", "500"), num("error_status", "lt", "600")), + rule(AccountTrafficOutcomeFailedOther, eq("has_error", "true")), + rule(AccountTrafficOutcomeFailedOther, eq("has_result", "false")), + rule(AccountTrafficOutcomeCompleted2xx, eq("ws", "false")), + rule(AccountTrafficOutcomeCompleted2xx, eq("terminal", "response.completed")), + rule(AccountTrafficOutcomeCompleted2xx, eq("terminal", "response.done")), + rule(AccountTrafficOutcomeUpstream429, eq("terminal", "response.failed"), eq("terminal_status", "429")), + rule(AccountTrafficOutcomeUpstream5xx, eq("terminal", "response.failed"), num("terminal_status", "gte", "500"), num("terminal_status", "lt", "600")), + }} +}() diff --git a/backend/internal/service/admin_observability_test.go b/backend/internal/service/admin_observability_test.go new file mode 100644 index 000000000000..9a3c7eefbab8 --- /dev/null +++ b/backend/internal/service/admin_observability_test.go @@ -0,0 +1,116 @@ +package service + +import ( + "context" + "strconv" + "testing" + + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" + "github.com/stretchr/testify/require" +) + +type trafficScopeCache struct { + beginIDs []int64 + snapshotIDs []int64 + outcomes []AccountTrafficOutcome +} + +func (c *trafficScopeCache) Begin(_ context.Context, id int64, _ AccountTrafficProtocol) error { + c.beginIDs = append(c.beginIDs, id) + return nil +} +func (c *trafficScopeCache) Finish(_ context.Context, _ int64, _ AccountTrafficProtocol, outcome AccountTrafficOutcome) error { + c.outcomes = append(c.outcomes, outcome) + return nil +} +func (c *trafficScopeCache) Snapshot(_ context.Context, id int64) (map[AccountTrafficProtocol]AccountTrafficObserveState, error) { + c.snapshotIDs = append(c.snapshotIDs, id) + return map[AccountTrafficProtocol]AccountTrafficObserveState{AccountTrafficProtocolHTTP: {Started: 1}}, nil +} + +func useAdminObservability(t *testing.T, config extensionv1.AdminObservabilityConfig) { + t.Helper() + previous := adminObservabilityConfigOverride.Load() + t.Cleanup(func() { adminObservabilityConfigOverride.Store(previous) }) + ConfigureAdminObservability(&config) +} + +func TestTrafficObservationFollowsTelemetrySwitch(t *testing.T) { + account := &Account{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth} + for _, enabled := range []bool{false, true} { + t.Run(strconv.FormatBool(enabled), func(t *testing.T) { + useAdminObservability(t, extensionv1.AdminObservabilityConfig{TelemetryEnabled: enabled}) + cache := &trafficScopeCache{} + observer := NewAccountTrafficObserver(cache, nil) + require.Equal(t, enabled, observer.Enabled()) + turn := observer.Begin(context.Background(), account, AccountTrafficProtocolWS) + snapshot, err := observer.Snapshot(context.Background(), account) + if !enabled { + require.Nil(t, turn) + require.ErrorIs(t, err, ErrAccountTrafficTelemetryUnavailable) + require.Empty(t, cache.beginIDs) + require.Empty(t, cache.snapshotIDs) + return + } + require.NotNil(t, turn) + require.NoError(t, err) + require.EqualValues(t, 1, snapshot[AccountTrafficProtocolHTTP].Started) + require.Equal(t, []int64{2}, cache.beginIDs) + require.Equal(t, []int64{2}, cache.snapshotIDs) + }) + } +} + +func TestTrafficObservationRejectsMissingAccountIdentity(t *testing.T) { + useAdminObservability(t, extensionv1.AdminObservabilityConfig{TelemetryEnabled: true}) + cache := &trafficScopeCache{} + observer := NewAccountTrafficObserver(cache, nil) + for _, account := range []*Account{ + nil, + {Platform: PlatformOpenAI, Type: AccountTypeOAuth}, + {ID: 2, Type: AccountTypeOAuth}, + {ID: 2, Platform: PlatformOpenAI}, + {ID: 2, Platform: "*", Type: AccountTypeOAuth}, + {ID: 2, Platform: PlatformOpenAI, Type: "*"}, + } { + require.Nil(t, observer.Begin(context.Background(), account, AccountTrafficProtocolHTTP)) + snapshot, err := observer.Snapshot(context.Background(), account) + require.ErrorIs(t, err, ErrAccountTrafficTelemetryUnavailable) + require.Nil(t, snapshot) + } + require.Empty(t, cache.beginIDs) + require.Empty(t, cache.snapshotIDs) +} + +func TestTrafficObservationFinishesStartedTurnAfterTelemetryIsSwitchedOff(t *testing.T) { + useAdminObservability(t, extensionv1.AdminObservabilityConfig{TelemetryEnabled: true}) + cache := &trafficScopeCache{} + observer := NewAccountTrafficObserver(cache, nil) + account := &Account{ID: 7, Platform: PlatformOpenAI, Type: AccountTypeOAuth} + turn := observer.Begin(context.Background(), account, AccountTrafficProtocolHTTP) + require.NotNil(t, turn) + ConfigureAdminObservability(&extensionv1.AdminObservabilityConfig{}) + turn.Finish(&OpenAIForwardResult{}, nil, false) + turn.Finish(nil, nil, false) + require.Equal(t, []AccountTrafficOutcome{AccountTrafficOutcomeCompleted2xx}, cache.outcomes) + require.Nil(t, observer.Begin(context.Background(), account, AccountTrafficProtocolHTTP)) + require.Len(t, cache.beginIDs, 1) +} + +func TestAccountTrafficOutcomeRulesKeepCancellationAboveErrorsAndRequireWSTerminals(t *testing.T) { + require.NoError(t, accountTrafficOutcomeRules.Validate()) + for _, tc := range []struct { + facts map[string]string + want AccountTrafficOutcome + }{ + {map[string]string{"client_cancelled": "true", "has_error": "true", "error_status": "503"}, AccountTrafficOutcomeCancelled}, + {map[string]string{"ws": "true", "terminal": "response.incomplete", "has_error": "true", "error_status": "429"}, AccountTrafficOutcomeCancelled}, + {map[string]string{"ws": "true", "has_result": "true", "has_error": "false", "terminal": ""}, AccountTrafficOutcomeFailedOther}, + {map[string]string{"ws": "true", "has_result": "true", "has_error": "false", "terminal": "response.failed", "terminal_status": "503"}, AccountTrafficOutcomeUpstream5xx}, + {map[string]string{"ws": "false", "has_result": "true", "has_error": "false"}, AccountTrafficOutcomeCompleted2xx}, + } { + got, err := accountTrafficOutcomeRules.Evaluate(tc.facts) + require.NoError(t, err) + require.Equal(t, string(tc.want), got) + } +} diff --git a/backend/internal/service/plugin_extension_runtime_test.go b/backend/internal/service/plugin_extension_runtime_test.go index 2986fc6a0c10..14ae7abd6d34 100644 --- a/backend/internal/service/plugin_extension_runtime_test.go +++ b/backend/internal/service/plugin_extension_runtime_test.go @@ -119,36 +119,6 @@ func TestPromptExtensionRuntimeUsesIndependentProcess(t *testing.T) { require.Greater(t, total, extensionv1.MaxPayloadBytes, "a complete seed larger than the RPC ceiling must remain loadable") } -func TestObservabilityExtensionRuntimeUsesScopedMetricsBroker(t *testing.T) { - binary := os.Getenv("SUB2API_ADMIN_OBSERVABILITY_TEST_BINARY") - if binary == "" { - t.Skip("set SUB2API_ADMIN_OBSERVABILITY_TEST_BINARY to the independent observability program") - } - data, err := os.ReadFile(binary) - require.NoError(t, err) - digest := sha256.Sum256(data) - installation := &PluginInstallation{ID: 13, PluginKey: "codexrip.admin-observability", Version: firstPartyTestVersion(t, "admin-observability"), BinaryPath: binary, BinarySHA256: hex.EncodeToString(digest[:]), Manifest: PluginManifest{Requires: PluginRequirements{ExtensionAPI: 1}, Capabilities: []PluginCapability{{ID: extensionv1.CapabilityObservability, Platform: "*", AccountType: "*"}, {ID: extensionv1.CapabilityAdmin, Platform: "*", AccountType: "*"}, {ID: extensionv1.CapabilityUI, Platform: "*", AccountType: "*"}}}} - directory := &credentialScopeDirectory{} - host := newPluginHostServiceServer(installation.PluginKey, nil, nil, PluginAccountScope{}) - host.extension = &pluginExtensionHost{key: installation.PluginKey, installation: installation, directory: directory, traffic: &trafficPolicyCache{}} - socketDir := filepath.Join(t.TempDir(), "runtime") - require.NoError(t, os.MkdirAll(socketDir, 0700)) - runtime, err := startPluginRuntime(context.Background(), installation, 15*time.Second, socketDir, host) - require.NoError(t, err) - t.Cleanup(runtime.kill) - require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{}`))) - out, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityAdmin, Operation: "telemetry.read", Payload: []byte(`{"account_id":7}`)}) - require.NoError(t, err) - require.Empty(t, out.Code) - var rows []extensionv1.AccountTrafficDisplay - require.NoError(t, json.Unmarshal(out.Payload, &rows)) - require.Len(t, rows, 1) - require.Equal(t, "http", rows[0].Protocol) - require.EqualValues(t, 3, rows[0].Finished) - require.InDelta(t, 2.0/3, *rows[0].CompletionRate, 1e-12) - require.Zero(t, directory.calls, "metrics access must not resolve credentials") -} - func TestCindyProviderExtensionRuntimeUsesIndependentProcess(t *testing.T) { binary := os.Getenv("SUB2API_CINDY_PROVIDER_TEST_BINARY") if binary == "" { diff --git a/backend/internal/service/plugin_manifest_schema_test.go b/backend/internal/service/plugin_manifest_schema_test.go index 02b6abc2dd18..ecc76786d28c 100644 --- a/backend/internal/service/plugin_manifest_schema_test.go +++ b/backend/internal/service/plugin_manifest_schema_test.go @@ -53,27 +53,6 @@ func TestPluginManifestSchemaDeclaresContributionFields(t *testing.T) { } } -func TestPluginManifestAcceptsRegisteredHyphenatedResources(t *testing.T) { - raw, err := os.ReadFile("../../../plugins/admin-observability/manifest.source.json") - if err != nil { - t.Fatal(err) - } - var source PluginManifest - if err := json.Unmarshal(raw, &source); err != nil { - t.Fatal(err) - } - if len(source.Resources) == 0 { - t.Fatal("the fixture must use the actual registered Prompt Audit resources") - } - manifest := testPluginManifest(nil) - manifest.Requires.ExtensionAPI = extensionv1.Version - manifest.Capabilities = source.Capabilities - manifest.Resources = source.Resources - if err := manifest.Validate(); err != nil { - t.Fatalf("existing registered resource names must be representable: %v", err) - } -} - func TestPluginManifestResourcePatternParityAndBoundaries(t *testing.T) { raw, err := os.ReadFile("../../pkg/pluginapi/v1/manifest.schema.json") if err != nil { diff --git a/backend/internal/service/plugin_observability.go b/backend/internal/service/plugin_observability.go deleted file mode 100644 index 04587bdc5a56..000000000000 --- a/backend/internal/service/plugin_observability.go +++ /dev/null @@ -1,47 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "time" - - "github.com/Wei-Shaw/sub2api/internal/config" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -func LegacyAdminObservabilityConfig(cfg *config.Config) extensionv1.AdminObservabilityConfig { - return extensionv1.AdminObservabilityConfig{TelemetryEnabled: cfg == nil || !cfg.Gateway.AccountTrafficTelemetryDisabled, ThemeEnabled: true} -} - -func currentTrafficObservationPolicy(ctx context.Context) (extensionv1.TrafficObservationPolicy, bool) { - return resolveTrafficObservationPolicy(ctx, "*", "*", 0) -} - -func currentAccountTrafficObservationPolicy(ctx context.Context, account *Account) (extensionv1.TrafficObservationPolicy, bool) { - if account == nil || account.ID <= 0 || account.Platform == "" || account.Platform == "*" || account.Type == "" || account.Type == "*" { - return extensionv1.TrafficObservationPolicy{}, false - } - return resolveTrafficObservationPolicy(ctx, account.Platform, account.Type, account.ID) -} - -func resolveTrafficObservationPolicy(ctx context.Context, platform, accountType string, accountID int64) (extensionv1.TrafficObservationPolicy, bool) { - if ctx == nil { - ctx = context.Background() - } - call, cancel := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel() - result, err := invokeProcessExtensionCached(call, platform, accountType, extensionv1.Invocation{Capability: extensionv1.CapabilityObservability, Operation: "observability.policy", AccountID: accountID, Payload: json.RawMessage(`{}`)}) - var policy extensionv1.TrafficObservationPolicy - if err != nil || result.Code != "" || json.Unmarshal(result.Payload, &policy) != nil || policy.Classification.Validate() != nil { - return policy, false - } - if !AccountTrafficOutcome(policy.Classification.Default).Valid() { - return policy, false - } - for _, rule := range policy.Classification.Rules { - if !AccountTrafficOutcome(rule.Result).Valid() { - return policy, false - } - } - return policy, true -} diff --git a/backend/internal/service/plugin_observability_test.go b/backend/internal/service/plugin_observability_test.go index 782e76cb830d..c9f73d9fa73a 100644 --- a/backend/internal/service/plugin_observability_test.go +++ b/backend/internal/service/plugin_observability_test.go @@ -5,151 +5,12 @@ import ( "encoding/json" "testing" - "github.com/Wei-Shaw/sub2api/internal/config" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -type trafficScopeCache struct { - beginIDs []int64 - protocols []AccountTrafficProtocol - snapshotIDs []int64 - finishedIDs []int64 -} - -func (c *trafficScopeCache) Begin(_ context.Context, id int64, protocol AccountTrafficProtocol) error { - c.beginIDs = append(c.beginIDs, id) - c.protocols = append(c.protocols, protocol) - return nil -} -func (c *trafficScopeCache) Finish(_ context.Context, id int64, _ AccountTrafficProtocol, _ AccountTrafficOutcome) error { - c.finishedIDs = append(c.finishedIDs, id) - return nil -} -func (c *trafficScopeCache) Snapshot(_ context.Context, id int64) (map[AccountTrafficProtocol]AccountTrafficObserveState, error) { - c.snapshotIDs = append(c.snapshotIDs, id) - return map[AccountTrafficProtocol]AccountTrafficObserveState{AccountTrafficProtocolHTTP: {Started: 1}}, nil -} - -type trafficPolicyCall struct { - platform, kind string - invocation extensionv1.Invocation -} - -// The embedded concrete manager supplies the full operation implementation; -// only the already-authorized process boundary is replaced by a fake RPC. -type trafficScopeInvoker struct { - *PluginManager - calls []trafficPolicyCall -} - -func (f *trafficScopeInvoker) InvokeCachedOperation(ctx context.Context, platform, kind string, in extensionv1.Invocation) (extensionv1.Result, error) { - f.calls = append(f.calls, trafficPolicyCall{platform, kind, in}) - return f.PluginManager.InvokeCachedOperation(ctx, platform, kind, in) -} - -var _ extensionv1.OperationInvoker = (*trafficScopeInvoker)(nil) -var _ extensionv1.CachedOperationInvoker = (*trafficScopeInvoker)(nil) - -func scopedTrafficObserver(t *testing.T, platform, kind string, percent int) (*AccountTrafficObserver, *trafficScopeCache, *trafficScopeInvoker, *int) { - t.Helper() - rpcCalls := new(int) - manager := ticketTestManager(t, config.OpenAICodexTicketConfig{}, func(in extensionv1.Invocation) (extensionv1.Result, error) { - *rpcCalls++ - return (promptPolicyFixture{}).InvokeOperation(context.Background(), "", "", in) - }) - installation := manager.extensions.Load().installations[1] - installation.Bindings = []PluginBinding{{Capability: extensionv1.CapabilityObservability, Platform: platform, AccountType: kind, Enabled: true, RolloutPercent: percent}} - installation.Manifest.Operations = map[string][]string{extensionv1.CapabilityObservability: {"observability.policy"}} - invoker := &trafficScopeInvoker{PluginManager: manager} - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) - processExtensionOperations.Store(&extensionOperationProvider{invoker: invoker}) - cache := &trafficScopeCache{} - return NewAccountTrafficObserver(cache, nil), cache, invoker, rpcCalls -} - -func TestTrafficObservationScopeUsesActualAccountForBeginAndSnapshot(t *testing.T) { - for _, tc := range []struct { - name, boundPlatform, boundType string - percent int - account Account - allowed bool - }{ - {"zero-rollout", "*", "*", 0, Account{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth}, false}, - {"half-included", "*", "*", 50, Account{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth}, true}, - {"half-excluded", "*", "*", 50, Account{ID: 3, Platform: PlatformOpenAI, Type: AccountTypeOAuth}, false}, - {"full-rollout", "*", "*", 100, Account{ID: 3, Platform: PlatformOpenAI, Type: AccountTypeOAuth}, true}, - {"scoped-included", PlatformOpenAI, AccountTypeOAuth, 100, Account{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeOAuth}, true}, - {"other-platform", PlatformOpenAI, AccountTypeOAuth, 100, Account{ID: 2, Platform: PlatformAnthropic, Type: AccountTypeOAuth}, false}, - {"other-type", PlatformOpenAI, AccountTypeOAuth, 100, Account{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, false}, - } { - for _, protocol := range AccountTrafficProtocols() { - t.Run(tc.name+"/begin-"+string(protocol), func(t *testing.T) { - observer, cache, invoker, rpcCalls := scopedTrafficObserver(t, tc.boundPlatform, tc.boundType, tc.percent) - turn := observer.Begin(context.Background(), &tc.account, protocol) - if tc.allowed { - require.NotNil(t, turn) - require.Equal(t, []int64{tc.account.ID}, cache.beginIDs) - require.Equal(t, []AccountTrafficProtocol{protocol}, cache.protocols) - require.Equal(t, 1, *rpcCalls) - } else { - require.Nil(t, turn, "out-of-scope account must not open a counted turn") - require.Empty(t, cache.beginIDs) - require.Zero(t, *rpcCalls, "excluded account must not reach the policy process") - } - require.Len(t, invoker.calls, 1) - require.Equal(t, tc.account.Platform, invoker.calls[0].platform) - require.Equal(t, tc.account.Type, invoker.calls[0].kind) - require.Equal(t, tc.account.ID, invoker.calls[0].invocation.AccountID) - require.JSONEq(t, `{}`, string(invoker.calls[0].invocation.Payload), "credentials/account bodies must not cross into classification policy") - }) - } - t.Run(tc.name+"/snapshot", func(t *testing.T) { - observer, cache, invoker, rpcCalls := scopedTrafficObserver(t, tc.boundPlatform, tc.boundType, tc.percent) - snapshot, err := observer.Snapshot(context.Background(), &tc.account) - if tc.allowed { - require.NoError(t, err) - require.EqualValues(t, 1, snapshot[AccountTrafficProtocolHTTP].Started) - require.Equal(t, []int64{tc.account.ID}, cache.snapshotIDs) - require.Equal(t, 1, *rpcCalls) - } else { - require.ErrorIs(t, err, ErrAccountTrafficTelemetryUnavailable) - require.Nil(t, snapshot) - require.Empty(t, cache.snapshotIDs) - require.Zero(t, *rpcCalls) - } - require.Len(t, invoker.calls, 1) - require.Equal(t, tc.account.Platform, invoker.calls[0].platform) - require.Equal(t, tc.account.Type, invoker.calls[0].kind) - require.Equal(t, tc.account.ID, invoker.calls[0].invocation.AccountID) - }) - } -} - -func TestTrafficObservationRejectsMissingAccountIdentity(t *testing.T) { - observer, cache, invoker, rpcCalls := scopedTrafficObserver(t, "*", "*", 100) - for _, account := range []*Account{ - nil, - {Platform: PlatformOpenAI, Type: AccountTypeOAuth}, - {ID: 2, Type: AccountTypeOAuth}, - {ID: 2, Platform: PlatformOpenAI}, - {ID: 2, Platform: "*", Type: AccountTypeOAuth}, - {ID: 2, Platform: PlatformOpenAI, Type: "*"}, - } { - require.Nil(t, observer.Begin(context.Background(), account, AccountTrafficProtocolHTTP)) - snapshot, err := observer.Snapshot(context.Background(), account) - require.ErrorIs(t, err, ErrAccountTrafficTelemetryUnavailable) - require.Nil(t, snapshot) - } - require.Empty(t, invoker.calls, "missing account facts must not become a shared policy lookup") - require.Zero(t, *rpcCalls) - require.Empty(t, cache.beginIDs) - require.Empty(t, cache.snapshotIDs) -} - type trafficPolicyCache struct { started int outcomes []AccountTrafficOutcome @@ -167,22 +28,6 @@ func (*trafficPolicyCache) Snapshot(context.Context, int64) (map[AccountTrafficP return map[AccountTrafficProtocol]AccountTrafficObserveState{AccountTrafficProtocolHTTP: {Started: 3, Completed2xx: 2, Upstream429: 1}, "unrecognized": {Started: 42}}, nil } -func TestTrafficObservationFinishesWithCapturedPolicyAfterPluginDisable(t *testing.T) { - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) - cache := &trafficPolicyCache{} - observer := NewAccountTrafficObserver(cache, nil) - account := &Account{ID: 7, Platform: PlatformOpenAI, Type: AccountTypeOAuth} - turn := observer.Begin(context.Background(), account, AccountTrafficProtocolHTTP) - require.NotNil(t, turn) - processExtensionOperations.Store(nil) - turn.Finish(&OpenAIForwardResult{}, nil, false) - turn.Finish(nil, nil, false) - require.Equal(t, []AccountTrafficOutcome{AccountTrafficOutcomeCompleted2xx}, cache.outcomes) - require.Nil(t, observer.Begin(context.Background(), account, AccountTrafficProtocolHTTP)) - require.Equal(t, 1, cache.started) -} - func TestMetricsBrokerRequiresCurrentObservabilityCapabilityAndExposesOnlyCounters(t *testing.T) { directory := &credentialScopeDirectory{} installation := &PluginInstallation{Manifest: PluginManifest{Capabilities: []PluginCapability{{ID: extensionv1.CapabilityAdmin, Platform: "*", AccountType: "*"}}}} diff --git a/backend/internal/service/plugin_prompt_fixture_test.go b/backend/internal/service/plugin_prompt_fixture_test.go index 64ecac9214b0..293122aab7b1 100644 --- a/backend/internal/service/plugin_prompt_fixture_test.go +++ b/backend/internal/service/plugin_prompt_fixture_test.go @@ -11,7 +11,6 @@ import ( codexrecovery "github.com/HTExplicit/sub2api-plugins/codexruntime/recovery" accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" - observability "github.com/HTExplicit/sub2api-plugins/adminobservability/policy" policy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" @@ -52,9 +51,6 @@ func (promptPolicyFixture) InvokeOperation(ctx context.Context, _ string, _ stri if strings.HasPrefix(in.Operation, "codex.identity.") { return codexprofile.Invoke(ctx, in) } - if strings.HasPrefix(in.Operation, "observability.") { - return observability.New().Invoke(ctx, in) - } if strings.HasPrefix(in.Operation, "cindy.") { module := cindy.New() raw, _ := json.Marshal(LegacyCindyProviderConfig()) diff --git a/backend/internal/service/setting_admin_observability.go b/backend/internal/service/setting_admin_observability.go new file mode 100644 index 000000000000..119c1740f3a7 --- /dev/null +++ b/backend/internal/service/setting_admin_observability.go @@ -0,0 +1,42 @@ +package service + +import ( + "context" + + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +// SettingKeyAdminObservabilityConfig stores the account traffic telemetry and +// flat theme switches formerly kept in the admin-observability plugin +// configuration. +const SettingKeyAdminObservabilityConfig = "admin_observability_config" + +// GetAdminObservabilityConfig returns the stored switches, or the deploy-time +// default when the administrator has not saved them yet. +func (s *SettingService) GetAdminObservabilityConfig(ctx context.Context) extensionv1.AdminObservabilityConfig { + stored := extensionv1.AdminObservabilityConfig{TelemetryEnabled: true, ThemeEnabled: true} + if s.readJSONSetting(ctx, SettingKeyAdminObservabilityConfig, &stored) { + return stored + } + if s == nil { + return LegacyAdminObservabilityConfig(nil) + } + return LegacyAdminObservabilityConfig(s.cfg) +} + +// LoadAdminObservabilityConfig installs the effective switches for this process +// at startup, including the deploy-time default. +func (s *SettingService) LoadAdminObservabilityConfig(ctx context.Context) { + config := s.GetAdminObservabilityConfig(ctx) + ConfigureAdminObservability(&config) +} + +// UpdateAdminObservabilityConfig persists the switches and applies them to this +// process. +func (s *SettingService) UpdateAdminObservabilityConfig(ctx context.Context, config extensionv1.AdminObservabilityConfig) error { + if err := s.writeJSONSetting(ctx, SettingKeyAdminObservabilityConfig, config); err != nil { + return err + } + ConfigureAdminObservability(&config) + return nil +} diff --git a/backend/internal/service/setting_image_tools.go b/backend/internal/service/setting_image_tools.go index 5b6e2bd6e97b..ac51c2c21086 100644 --- a/backend/internal/service/setting_image_tools.go +++ b/backend/internal/service/setting_image_tools.go @@ -2,9 +2,6 @@ package service import ( "context" - "encoding/json" - "errors" - "strings" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) @@ -13,27 +10,11 @@ import ( // switches formerly kept in the image-tools plugin configuration. const SettingKeyImageToolsConfig = "image_tools_config" -func (s *SettingService) storedImageToolsConfig(ctx context.Context) (extensionv1.ImageToolsConfig, bool) { - if s == nil || s.settingRepo == nil { - return extensionv1.ImageToolsConfig{}, false - } - dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) - defer cancel() - raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyImageToolsConfig) - if err != nil || strings.TrimSpace(raw) == "" { - return extensionv1.ImageToolsConfig{}, false - } - var stored extensionv1.ImageToolsConfig - if json.Unmarshal([]byte(raw), &stored) != nil { - return extensionv1.ImageToolsConfig{}, false - } - return stored, true -} - // GetImageToolsConfig returns the stored switches, or the deploy-time rollout // flags when the administrator has not saved them yet. func (s *SettingService) GetImageToolsConfig(ctx context.Context) extensionv1.ImageToolsConfig { - if stored, ok := s.storedImageToolsConfig(ctx); ok { + var stored extensionv1.ImageToolsConfig + if s.readJSONSetting(ctx, SettingKeyImageToolsConfig, &stored) { return stored } return LegacyImageToolsConfig() @@ -41,7 +22,8 @@ func (s *SettingService) GetImageToolsConfig(ctx context.Context) extensionv1.Im // LoadImageToolsConfig installs the stored switches for this process at startup. func (s *SettingService) LoadImageToolsConfig(ctx context.Context) { - if stored, ok := s.storedImageToolsConfig(ctx); ok { + var stored extensionv1.ImageToolsConfig + if s.readJSONSetting(ctx, SettingKeyImageToolsConfig, &stored) { ConfigureImageTools(&stored) } } @@ -49,16 +31,7 @@ func (s *SettingService) LoadImageToolsConfig(ctx context.Context) { // UpdateImageToolsConfig persists the switches and applies them to this process. // Image Studio's background runtime still starts only at boot, as before. func (s *SettingService) UpdateImageToolsConfig(ctx context.Context, config extensionv1.ImageToolsConfig) error { - if s == nil || s.settingRepo == nil { - return errors.New("settings are unavailable") - } - raw, err := json.Marshal(config) - if err != nil { - return err - } - dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) - defer cancel() - if err := s.settingRepo.Set(dbCtx, SettingKeyImageToolsConfig, string(raw)); err != nil { + if err := s.writeJSONSetting(ctx, SettingKeyImageToolsConfig, config); err != nil { return err } ConfigureImageTools(&config) diff --git a/backend/internal/service/setting_json.go b/backend/internal/service/setting_json.go new file mode 100644 index 000000000000..50e53243b98d --- /dev/null +++ b/backend/internal/service/setting_json.go @@ -0,0 +1,37 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "strings" +) + +// readJSONSetting decodes a JSON-valued setting into target. It reports false +// when the setting is unset, unreadable or not valid JSON for target. +func (s *SettingService) readJSONSetting(ctx context.Context, key string, target any) bool { + if s == nil || s.settingRepo == nil { + return false + } + dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) + defer cancel() + raw, err := s.settingRepo.GetValue(dbCtx, key) + if err != nil || strings.TrimSpace(raw) == "" { + return false + } + return json.Unmarshal([]byte(raw), target) == nil +} + +// writeJSONSetting stores value as a JSON-valued setting. +func (s *SettingService) writeJSONSetting(ctx context.Context, key string, value any) error { + if s == nil || s.settingRepo == nil { + return errors.New("settings are unavailable") + } + raw, err := json.Marshal(value) + if err != nil { + return err + } + dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) + defer cancel() + return s.settingRepo.Set(dbCtx, key, string(raw)) +} diff --git a/backend/internal/service/setting_public.go b/backend/internal/service/setting_public.go index d76ce1a30d39..397c9def8fed 100644 --- a/backend/internal/service/setting_public.go +++ b/backend/internal/service/setting_public.go @@ -366,6 +366,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings AvailableChannelsEnabled: settings[SettingKeyAvailableChannelsEnabled] == "true", ImageStudioEnabled: CindyImageStudioFeatureEnabled(), + FlatThemeEnabled: FlatThemeEnabled(), SubscriptionEnabled: !isFalseSettingValue(settings[SettingKeySubscriptionEnabled]), @@ -638,6 +639,7 @@ type PublicSettingsInjectionPayload struct { ChannelMonitorHideUserRanking bool `json:"channel_monitor_hide_user_ranking"` AvailableChannelsEnabled bool `json:"available_channels_enabled"` ImageStudioEnabled bool `json:"image_studio_enabled"` + FlatThemeEnabled bool `json:"flat_theme_enabled"` SubscriptionEnabled bool `json:"subscription_enabled"` ModelPlazaEnabled bool `json:"model_plaza_enabled"` ModelPlazaRequireAuth bool `json:"model_plaza_require_auth"` @@ -723,6 +725,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any ChannelMonitorHideUserRanking: settings.ChannelMonitorHideUserRanking, AvailableChannelsEnabled: settings.AvailableChannelsEnabled, ImageStudioEnabled: settings.ImageStudioEnabled, + FlatThemeEnabled: settings.FlatThemeEnabled, SubscriptionEnabled: settings.SubscriptionEnabled, ModelPlazaEnabled: settings.ModelPlazaEnabled, ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth, diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index c7157f04a75e..78e608b63620 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -410,6 +410,7 @@ type PublicSettings struct { // Available Channels feature (user-facing aggregate view) AvailableChannelsEnabled bool `json:"available_channels_enabled"` ImageStudioEnabled bool `json:"image_studio_enabled"` + FlatThemeEnabled bool `json:"flat_theme_enabled"` // Subscription feature switch (see SystemSettings.SubscriptionEnabled) SubscriptionEnabled bool `json:"subscription_enabled"` diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 711eca49fde8..261eeb2d08bd 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -920,8 +920,10 @@ func ProvideOpsIngressRejectAggregator(opsRepo OpsRepository, opsService *OpsSer // ProvideSettingService wires SettingService with group reader and proxy repo. func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupRepository, proxyRepo ProxyRepository, cfg *config.Config) *SettingService { svc := NewSettingService(settingRepo, cfg) - // Image tool switches are read by package-level gates before Image Studio starts. + // Image tool and observability switches are read by package-level gates + // before Image Studio and the gateway start. svc.LoadImageToolsConfig(context.Background()) + svc.LoadAdminObservabilityConfig(context.Background()) svc.SetDefaultSubscriptionGroupReader(groupRepo) svc.SetProxyRepository(proxyRepo) if err := svc.LoadForwardedClientIPSettings(context.Background()); err != nil { diff --git a/backend/internal/testextensions/fixtures.go b/backend/internal/testextensions/fixtures.go index 61803922a394..c610d02ddf77 100644 --- a/backend/internal/testextensions/fixtures.go +++ b/backend/internal/testextensions/fixtures.go @@ -13,7 +13,6 @@ import ( codexrecovery "github.com/HTExplicit/sub2api-plugins/codexruntime/recovery" accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" - observability "github.com/HTExplicit/sub2api-plugins/adminobservability/policy" prompt "github.com/HTExplicit/sub2api-plugins/promptskills/policy" "github.com/Wei-Shaw/sub2api/internal/service" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" @@ -38,9 +37,6 @@ func (operations) InvokeOperation(ctx context.Context, _, _ string, in extension raw, err := json.Marshal(codexprofile.TransportPlan(query, false)) return extensionv1.Result{Payload: raw}, err } - if strings.HasPrefix(in.Operation, "observability.") { - return observability.New().Invoke(ctx, in) - } if strings.HasPrefix(in.Operation, "cindy.") { module := cindy.New() raw, _ := json.Marshal(service.LegacyCindyProviderConfig()) @@ -60,6 +56,7 @@ func (operations) InvokeOperation(ctx context.Context, _, _ string, in extension func Install() { service.ConfigureProcessExtensionServices(nil, operations{}) service.ConfigureImageTools(nil) + service.ConfigureAdminObservability(nil) } func InstallImageTools(config extensionv1.ImageToolsConfig) { diff --git a/frontend/public/fonts/misans/NOTICE.md b/frontend/public/fonts/misans/NOTICE.md index 7596ff7ca8ce..d3f1d89f4925 100644 --- a/frontend/public/fonts/misans/NOTICE.md +++ b/frontend/public/fonts/misans/NOTICE.md @@ -4,7 +4,7 @@ Copyright Xiaomi. Distributed under the MiSans Font Intellectual Property Licens Original, unmodified WOFF2 files extracted from the official [MiSans download](https://hyperos.mi.com/font-download/MiSans.zip). No subsetting, conversion or glyph modifications were performed. -The official license is included as [MiSans-Font-Intellectual-Property-License.pdf](MiSans-Font-Intellectual-Property-License.pdf). Fonts are included in the signed admin-observability plugin and loaded with `font-display: swap` while its theme is enabled. +The official license is included as [MiSans-Font-Intellectual-Property-License.pdf](MiSans-Font-Intellectual-Property-License.pdf). The fonts live in `frontend/src/assets/fonts/misans/` and are loaded with `font-display: swap` while the flat theme is enabled. ## Original font checksums diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 9cb6c7d8aa7a..ad985a0ab12c 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -1164,6 +1164,22 @@ export async function updateImageToolsSettings(settings: ImageToolsSettings): Pr return data; } +export interface ObservabilitySettings { + telemetry_enabled: boolean + theme_enabled: boolean +} + +/** Account traffic telemetry and flat site theme switches. */ +export async function getObservabilitySettings(): Promise { + const { data } = await apiClient.get("/admin/settings/observability"); + return data; +} + +export async function updateObservabilitySettings(settings: ObservabilitySettings): Promise { + const { data } = await apiClient.put("/admin/settings/observability", settings); + return data; +} + /** Read-only release-pinned official model capacity catalog. */ export async function getOfficialModelCapacityCatalog(): Promise { const { data } = await apiClient.get("/admin/settings/model-context-catalog"); @@ -1689,6 +1705,8 @@ export const settingsAPI = { getOfficialModelCapacityCatalog, getImageToolsSettings, updateImageToolsSettings, + getObservabilitySettings, + updateObservabilitySettings, updateSettings, testSmtpConnection, sendTestEmail, diff --git a/plugins/admin-observability/ui/assets/fonts/MiSans-Bold.woff2 b/frontend/src/assets/fonts/misans/MiSans-Bold.woff2 similarity index 100% rename from plugins/admin-observability/ui/assets/fonts/MiSans-Bold.woff2 rename to frontend/src/assets/fonts/misans/MiSans-Bold.woff2 diff --git a/plugins/admin-observability/ui/assets/fonts/MiSans-Medium.woff2 b/frontend/src/assets/fonts/misans/MiSans-Medium.woff2 similarity index 100% rename from plugins/admin-observability/ui/assets/fonts/MiSans-Medium.woff2 rename to frontend/src/assets/fonts/misans/MiSans-Medium.woff2 diff --git a/plugins/admin-observability/ui/assets/fonts/MiSans-Regular.woff2 b/frontend/src/assets/fonts/misans/MiSans-Regular.woff2 similarity index 100% rename from plugins/admin-observability/ui/assets/fonts/MiSans-Regular.woff2 rename to frontend/src/assets/fonts/misans/MiSans-Regular.woff2 diff --git a/plugins/admin-observability/ui/assets/fonts/MiSans-Semibold.woff2 b/frontend/src/assets/fonts/misans/MiSans-Semibold.woff2 similarity index 100% rename from plugins/admin-observability/ui/assets/fonts/MiSans-Semibold.woff2 rename to frontend/src/assets/fonts/misans/MiSans-Semibold.woff2 diff --git a/frontend/src/components/admin/ObservabilitySettingsPanel.vue b/frontend/src/components/admin/ObservabilitySettingsPanel.vue new file mode 100644 index 000000000000..8d9e4ff2b247 --- /dev/null +++ b/frontend/src/components/admin/ObservabilitySettingsPanel.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index df0e2780635a..85fd253dbb85 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -881,7 +881,7 @@ const adminNavItems = computed((): NavItem[] => { featureFlag: flagRiskControl, children: [ { path: '/admin/risk-control', label: t('nav.contentModeration'), icon: ShieldIcon }, - { path: '/admin/prompt-audit', label: t('nav.promptAudit'), icon: ShieldIcon, featureFlag: () => pluginExtensions.items.some(item => item.slot === 'surface' && item.id === 'prompt-audit') }, + { path: '/admin/prompt-audit', label: t('nav.promptAudit'), icon: ShieldIcon }, ], }, { path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true }, diff --git a/frontend/src/components/payment/__tests__/StripePaymentInline.contrast.spec.ts b/frontend/src/components/payment/__tests__/StripePaymentInline.contrast.spec.ts index eb54672c45dd..1a0462ae0dc8 100644 --- a/frontend/src/components/payment/__tests__/StripePaymentInline.contrast.spec.ts +++ b/frontend/src/components/payment/__tests__/StripePaymentInline.contrast.spec.ts @@ -91,7 +91,7 @@ describe('StripePaymentInline amount contrast', () => { it('keeps the solid fill independent of the flat theme gradient token, even when the gradient is disabled', async () => { const header = await amountHeader() - const theme = postcss.parse(readFileSync(resolve(__dirname, '../../../../../plugins/admin-observability/ui/assets/theme.css'), 'utf8')) + const theme = postcss.parse(readFileSync(resolve(__dirname, '../../../styles/flat-theme.css'), 'utf8')) const headerElement = header.element as HTMLElement let flatGradient = '' theme.walkDecls('--theme-background-gradient-to-br', declaration => { flatGradient = declaration.value }) diff --git a/frontend/src/features/prompt-audit/PromptAuditView.vue b/frontend/src/features/prompt-audit/PromptAuditView.vue index 0f5c642e3391..1537a8c31ec8 100644 --- a/frontend/src/features/prompt-audit/PromptAuditView.vue +++ b/frontend/src/features/prompt-audit/PromptAuditView.vue @@ -1,5 +1,481 @@ - + + diff --git a/plugins/admin-observability/ui/src/__tests__/PromptAuditView.spec.ts b/frontend/src/features/prompt-audit/__tests__/PromptAuditView.spec.ts similarity index 99% rename from plugins/admin-observability/ui/src/__tests__/PromptAuditView.spec.ts rename to frontend/src/features/prompt-audit/__tests__/PromptAuditView.spec.ts index 91d496ed957b..8897e081542b 100644 --- a/plugins/admin-observability/ui/src/__tests__/PromptAuditView.spec.ts +++ b/frontend/src/features/prompt-audit/__tests__/PromptAuditView.spec.ts @@ -12,7 +12,7 @@ const mocks = vi.hoisted(() => ({ })) vi.mock('../api', () => ({ default: mocks })) -vi.mock('@sub2api/plugin-ui', async importOriginal => ({ ...await importOriginal(), useNotifications: () => ({ showSuccess: mocks.showSuccess, showError: mocks.showError }) })) +vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showSuccess: mocks.showSuccess, showError: mocks.showError }) })) vi.mock('vue-i18n', async () => { const actual = await vi.importActual('vue-i18n') return { ...actual, useI18n: () => ({ locale: { value: 'en' }, t: (key: string, params?: Record) => key.replace(/\{(\w+)\}/g, (_, token) => String(params?.[token] ?? `{${token}}`)) }) } diff --git a/frontend/src/features/prompt-audit/__tests__/api.spec.ts b/frontend/src/features/prompt-audit/__tests__/api.spec.ts new file mode 100644 index 000000000000..ead59e2f3c4b --- /dev/null +++ b/frontend/src/features/prompt-audit/__tests__/api.spec.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { emptyEventFilters } from '../viewModel' + +const client = vi.hoisted(() => ({ get: vi.fn(), put: vi.fn(), post: vi.fn(), delete: vi.fn() })) +vi.mock('@/api/client', () => ({ apiClient: client })) + +import promptAuditAPI from '../api' + +describe('Prompt Audit API', () => { + beforeEach(() => Object.values(client).forEach((mock) => mock.mockReset())) + + it('uses the independent admin route namespace', async () => { + client.get.mockResolvedValue({ data: { config_version: 1 } }) + await promptAuditAPI.getConfig() + expect(client.get).toHaveBeenCalledWith('/admin/prompt-audit/config') + + client.get.mockResolvedValue({ data: { process_status: 'running' } }) + await promptAuditAPI.getRuntime() + expect(client.get).toHaveBeenCalledWith('/admin/prompt-audit/runtime') + }) + + it('sends a temporary probe token only in the request and never invents response credentials', async () => { + client.post.mockResolvedValue({ data: { ok: true, token_applied: true } }) + const result = await promptAuditAPI.probeEndpoint({ + id: 'guard-1', name: 'Guard', protocol: 'openai_compatible', base_url: 'http://127.0.0.1:8000', model: 'guard', + token: 'api-canary-secret', clear_token: false, timeout_ms: 1000, input_limit: 1000, enabled: true, has_token: false, token_status: 'missing', + }) + expect(client.post).toHaveBeenCalledWith('/admin/prompt-audit/endpoints/probe', expect.objectContaining({ endpoint: expect.objectContaining({ token: 'api-canary-secret' }) })) + expect(JSON.stringify(result)).not.toContain('api-canary-secret') + }) + + it('passes a server preview token through the confirmed filter-delete contract', async () => { + client.post.mockResolvedValue({ data: { deleted_events: 2, deleted_jobs: 2 } }) + const filters = emptyEventFilters() + filters.start_at = '2026-07-15T00:00' + filters.end_at = '2026-07-16T00:00' + await promptAuditAPI.deleteEventsByFilter(filters, { + matched_count: 2, filter_summary: {}, snapshot_max_id: 10, filter_hash: 'a'.repeat(64), confirmation_token: 'opaque-token', expires_at: '2026-07-16T00:05:00Z', + }) + expect(client.post).toHaveBeenCalledWith('/admin/prompt-audit/events/delete-by-filter', expect.objectContaining({ + snapshot_max_id: 10, filter_hash: 'a'.repeat(64), confirmation_token: 'opaque-token', confirm: true, + })) + }) +}) diff --git a/plugins/admin-observability/ui/src/__tests__/components.spec.ts b/frontend/src/features/prompt-audit/__tests__/components.spec.ts similarity index 99% rename from plugins/admin-observability/ui/src/__tests__/components.spec.ts rename to frontend/src/features/prompt-audit/__tests__/components.spec.ts index 7bfe449f43d6..e81f3544ab04 100644 --- a/plugins/admin-observability/ui/src/__tests__/components.spec.ts +++ b/frontend/src/features/prompt-audit/__tests__/components.spec.ts @@ -10,9 +10,7 @@ import type { PromptAuditDraft, PromptAuditEndpointDraft, PromptAuditEvent, Prom import { emptyEventFilters, resolveDeleteRangeFilters, SCANNER_CATALOG } from '../viewModel' const confirmAction = vi.hoisted(() => vi.fn()) -vi.mock('@sub2api/plugin-ui', async importOriginal => ({ - ...await importOriginal(), confirmAction, -})) +vi.mock('@/utils/browserActions', () => ({ confirmAction })) vi.mock('vue-i18n', async () => { const actual = await vi.importActual('vue-i18n') diff --git a/frontend/src/features/prompt-audit/__tests__/integrationSurface.spec.ts b/frontend/src/features/prompt-audit/__tests__/integrationSurface.spec.ts index 66c96ea23415..3dd006ad9dd9 100644 --- a/frontend/src/features/prompt-audit/__tests__/integrationSurface.spec.ts +++ b/frontend/src/features/prompt-audit/__tests__/integrationSurface.spec.ts @@ -4,8 +4,6 @@ import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import en from '@/i18n/locales/en' import zh from '@/i18n/locales/zh' -import pluginEn from '../../../../../plugins/admin-observability/ui/src/locales/en' -import pluginZh from '../../../../../plugins/admin-observability/ui/src/locales/zh' const here = dirname(fileURLToPath(import.meta.url)) const read = (path: string) => readFileSync(resolve(here, path), 'utf8') @@ -29,11 +27,11 @@ describe('Prompt Audit integration surface', () => { }) it('keeps Prompt Audit locale trees symmetric and all operational controls named', () => { - expect(Object.keys(pluginZh.admin.promptAudit)).toEqual(Object.keys(pluginEn.admin.promptAudit)) + expect(Object.keys(zh.admin.promptAudit)).toEqual(Object.keys(en.admin.promptAudit)) expect(zh.nav.securityAudit).toBeTruthy() expect(en.nav.securityAudit).toBeTruthy() - const endpoint = read('../../../../../plugins/admin-observability/ui/src/components/EndpointPool.vue') - const events = read('../../../../../plugins/admin-observability/ui/src/components/EventWorkspace.vue') + const endpoint = read('../components/EndpointPool.vue') + const events = read('../components/EventWorkspace.vue') expect(endpoint).toContain('aria-label') expect(events).toContain('aria-label') expect(events).toContain('overflow-x-auto') diff --git a/plugins/admin-observability/ui/src/__tests__/viewModel.spec.ts b/frontend/src/features/prompt-audit/__tests__/viewModel.spec.ts similarity index 100% rename from plugins/admin-observability/ui/src/__tests__/viewModel.spec.ts rename to frontend/src/features/prompt-audit/__tests__/viewModel.spec.ts diff --git a/frontend/src/features/prompt-audit/api.ts b/frontend/src/features/prompt-audit/api.ts new file mode 100644 index 000000000000..05fe2aa3898d --- /dev/null +++ b/frontend/src/features/prompt-audit/api.ts @@ -0,0 +1,120 @@ +import { apiClient } from '@/api/client' +import type { + PromptAuditConfig, + PromptAuditEvent, + PromptAuditGroup, + PromptAuditRuntime, + PromptAuditUpdateRequest, + PromptDeletePreview, + PromptDeleteResult, + PromptEventFilters, + PromptEventPage, + PromptProbeResult, + PromptAuditEndpointDraft, +} from './types' +import { eventFilterPayload, eventQueryParams } from './viewModel' + +const basePath = '/admin/prompt-audit' + +export async function getConfig(): Promise { + const { data } = await apiClient.get(`${basePath}/config`) + return data +} + +export async function updateConfig(payload: PromptAuditUpdateRequest): Promise { + const { data } = await apiClient.put(`${basePath}/config`, payload) + return data +} + +export async function probeEndpoint(endpoint: PromptAuditEndpointDraft): Promise { + const { data } = await apiClient.post(`${basePath}/endpoints/probe`, { + endpoint: { + id: endpoint.id, + name: endpoint.name, + protocol: 'openai_compatible', + base_url: endpoint.base_url, + model: endpoint.model, + token: endpoint.token || undefined, + timeout_ms: endpoint.timeout_ms, + input_limit: endpoint.input_limit, + enabled: endpoint.enabled, + }, + }) + return data +} + +export async function getRuntime(): Promise { + const { data } = await apiClient.get(`${basePath}/runtime`) + return data +} + +export async function listEvents( + filters: PromptEventFilters, + page: number, + pageSize: number, +): Promise { + const { data } = await apiClient.get(`${basePath}/events`, { + params: { page, page_size: pageSize, ...eventQueryParams(filters) }, + }) + return data +} + +export async function getEvent(id: number): Promise { + const { data } = await apiClient.get(`${basePath}/events/${id}`) + return data +} + +export async function deleteEvent(id: number): Promise { + const { data } = await apiClient.delete(`${basePath}/events/${id}`) + return data +} + +export async function batchDeleteEvents(ids: number[]): Promise { + const { data } = await apiClient.post(`${basePath}/events/batch-delete`, { ids }) + return data +} + +export async function previewDelete(filters: PromptEventFilters): Promise { + const { data } = await apiClient.post( + `${basePath}/events/delete-preview`, + eventFilterPayload(filters), + ) + return data +} + +export async function deleteEventsByFilter( + filters: PromptEventFilters, + preview: PromptDeletePreview, +): Promise { + const { data } = await apiClient.post(`${basePath}/events/delete-by-filter`, { + filter: eventFilterPayload(filters), + snapshot_max_id: preview.snapshot_max_id, + filter_hash: preview.filter_hash, + confirmation_token: preview.confirmation_token, + confirm: true, + }) + return data +} + +export async function listGroups(): Promise { + const { data } = await apiClient.get('/admin/groups/all', { + params: { include_inactive: true }, + }) + return data +} + +export const promptAuditAPI = { + getConfig, + updateConfig, + probeEndpoint, + getRuntime, + listEvents, + getEvent, + deleteEvent, + batchDeleteEvents, + previewDelete, + deleteEventsByFilter, + listGroups, +} + +export default promptAuditAPI diff --git a/plugins/admin-observability/ui/src/components/EndpointPool.vue b/frontend/src/features/prompt-audit/components/EndpointPool.vue similarity index 99% rename from plugins/admin-observability/ui/src/components/EndpointPool.vue rename to frontend/src/features/prompt-audit/components/EndpointPool.vue index 6bf2a78a1ccb..e67ff74c80c2 100644 --- a/plugins/admin-observability/ui/src/components/EndpointPool.vue +++ b/frontend/src/features/prompt-audit/components/EndpointPool.vue @@ -141,7 +141,8 @@ ' -theme = ROOT / "plugins/admin-observability/ui/assets/theme.css" -observed, errors, external = [], [], [] -with sync_playwright() as playwright: - browser = playwright.chromium.launch() - try: - page = browser.new_page() - page.on("pageerror", lambda error: errors.append(str(error))) - def serve(route): - parsed = urlsplit(route.request.url) - if parsed.hostname != "colors.test": - external.append(route.request.url) - route.abort() - elif parsed.path == "/": - route.fulfill(body=html, content_type="text/html") - elif parsed.path == "/probe.js": - route.fulfill(body=bundle.read_bytes(), content_type="text/javascript") - elif parsed.path == "/theme.css": - route.fulfill(body=theme.read_bytes(), content_type="text/css") - else: - route.fulfill(status=404) - page.route("**/*", serve) - page.goto("https://colors.test/") - for mode, text, grid in [("light-core", "#374151", "#e5e7eb"), ("light-plugin", "#3a3b40", "#dcdcdc"), ("dark-plugin", "#dcdcdc", "#3a3b40"), ("dark-core", "#e5e7eb", "#374151")]: - if mode == "light-plugin": - page.evaluate("() => { const link=document.createElement('link');link.id='theme';link.rel='stylesheet';link.href='/theme.css';document.head.append(link) }") - elif mode == "dark-plugin": - page.evaluate("document.documentElement.classList.add('dark')") - elif mode == "dark-core": - page.evaluate("document.getElementById('theme').remove()") - page.wait_for_function("expected => { const colors=JSON.parse(document.querySelector('#colors').textContent);return colors.text===expected[0]&&colors.grid===expected[1] }", arg=[text, grid]) - observed.append({"mode": mode, **json.loads(page.locator("#colors").inner_text())}) - page.evaluate("document.documentElement.style.setProperty('--theme-chart-text','url(https://invalid.test)')") - page.wait_for_function("JSON.parse(document.querySelector('#colors').textContent).text==='#e5e7eb'") - page.evaluate("unmountProbe();document.documentElement.classList.remove('dark')") - assert not errors and not external, (errors, external) - finally: - browser.close() -report = {"offline_only": True, "cases": observed, "page_errors": errors, "external_requests": external, "source_sha256": hashlib.sha256((ROOT / "frontend/src/composables/usePresentationColors.ts").read_bytes()).hexdigest(), "theme_sha256": hashlib.sha256(theme.read_bytes()).hexdigest()} -(args.output / "chart-colors.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") -print(json.dumps(report)) diff --git a/plugins/admin-observability/scripts/check-theme.py b/plugins/admin-observability/scripts/check-theme.py deleted file mode 100644 index 8ea423c30181..000000000000 --- a/plugins/admin-observability/scripts/check-theme.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Offline browser acceptance of compiled host CSS and the optional theme.""" -import argparse -import json -from pathlib import Path -from urllib.parse import urlparse - -from playwright.sync_api import sync_playwright - -parser = argparse.ArgumentParser() -parser.add_argument("--core-css", type=Path, required=True) -parser.add_argument("--evidence-dir", type=Path, required=True) -args = parser.parse_args() -module = Path(__file__).resolve().parents[1] -args.evidence_dir.mkdir(parents=True, exist_ok=True) -html = """ -

插件主题 · Theme

-

账号与用量 · Account and usage

-
状态与进度 · Status and progress
-
""" -rows = [] -page_errors = [] -external_requests = [] - -with sync_playwright() as playwright: - browser = playwright.chromium.launch(headless=True) - try: - for width in [900, 390]: - for dark in [False, True]: - page = browser.new_page(viewport={"width": width, "height": 650}) - page.on("pageerror", lambda error: page_errors.append(str(error))) - - def serve(route): - parsed = urlparse(route.request.url) - if parsed.hostname != "theme.test": - external_requests.append(route.request.url) - route.abort() - return - if parsed.path == "/": - route.fulfill(body=html, content_type="text/html") - elif parsed.path == "/core.css": - route.fulfill(body=args.core_css.read_bytes(), content_type="text/css") - elif parsed.path == "/theme.css": - route.fulfill(body=(module / "ui/assets/theme.css").read_bytes(), content_type="text/css") - elif parsed.path.startswith("/fonts/") and parsed.path.split("/")[-1] in ["MiSans-Regular.woff2", "MiSans-Medium.woff2", "MiSans-Semibold.woff2", "MiSans-Bold.woff2"]: - route.fulfill(body=(module / "ui/assets/fonts" / parsed.path.split("/")[-1]).read_bytes(), content_type="font/woff2") - else: - route.fulfill(status=404) - - page.route("**/*", serve) - page.goto("https://theme.test/", wait_until="networkidle") - page.evaluate("dark => document.documentElement.classList.toggle('dark', dark)", dark) - for enabled in [False, True, False]: - if enabled: - page.evaluate("""() => new Promise((resolve, reject) => { const link=document.createElement('link');link.id='theme';link.rel='stylesheet';link.href='/theme.css';link.onload=resolve;link.onerror=reject;document.head.append(link) })""") - page.evaluate("() => document.fonts.ready") - else: - page.evaluate("() => document.getElementById('theme')?.remove()") - # Buttons and cards transition their colors/radii. Wait for - # the actual cascade to settle after adding/removing CSS. - page.wait_for_function("""enabled => { - const button=getComputedStyle(document.querySelector('.btn-primary')); - const card=getComputedStyle(document.querySelector('.card')); - return button.backgroundColor === (enabled ? 'rgb(22, 138, 73)' : 'rgb(20, 184, 166)') && card.borderRadius === (enabled ? '0px' : '16px'); - }""", arg=enabled, timeout=2000) - observed = page.evaluate("""() => ({canvas:getComputedStyle(document.body).backgroundColor,font:getComputedStyle(document.body).fontFamily,cardRadius:getComputedStyle(document.querySelector('.card')).borderRadius,inputRadius:getComputedStyle(document.querySelector('.input')).borderRadius,buttonRadius:getComputedStyle(document.querySelector('.btn')).borderRadius,accent:getComputedStyle(document.querySelector('.btn-primary')).backgroundColor,gradient:getComputedStyle(document.getElementById('gradient')).backgroundImage,overflow:document.documentElement.scrollWidth>innerWidth,input:document.querySelector('input').value})""") - assert observed["accent"] == ("rgb(22, 138, 73)" if enabled else "rgb(20, 184, 166)"), observed - assert observed["canvas"] == ({(False, False): "rgb(249, 250, 251)", (False, True): "rgb(2, 6, 23)", (True, False): "rgb(255, 255, 255)", (True, True): "rgb(17, 17, 17)"}[(enabled, dark)]), observed - assert observed["cardRadius"] == ("0px" if enabled else "16px"), observed - assert observed["buttonRadius"] == ("0px" if enabled else "12px"), observed - assert ("MiSans" in observed["font"]) == enabled, observed - assert not observed["overflow"] and observed["input"] == "saved input", observed - if enabled: - assert observed["gradient"].count("rgb(22, 138, 73)") == 2, observed - rows.append({"width": width, "dark": dark, "enabled": enabled, **observed}) - if enabled: - page.screenshot(path=str(args.evidence_dir / f"theme-{width}-{'dark' if dark else 'light'}.png")) - page.close() - finally: - browser.close() - -assert not page_errors and not external_requests, (page_errors, external_requests) -report = {"cases": rows, "page_errors": page_errors, "external_requests": external_requests, "visual_review": "computed styles and geometry; screenshots captured without pixel review"} -(args.evidence_dir / "theme-check.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") -print(json.dumps({"passed_cases": len(rows), "page_errors": len(page_errors), "external_requests": len(external_requests)})) diff --git a/plugins/admin-observability/scripts/check-ui.py b/plugins/admin-observability/scripts/check-ui.py deleted file mode 100644 index 75704eb189f2..000000000000 --- a/plugins/admin-observability/scripts/check-ui.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Offline browser acceptance of the independently packaged prompt audit UI.""" -import argparse -import importlib.util -import json -import threading -from http.server import ThreadingHTTPServer -from pathlib import Path -from urllib.parse import urlsplit - -from playwright.sync_api import expect, sync_playwright - -ROOT = Path(__file__).resolve().parents[3] -spec = importlib.util.spec_from_file_location("prompt_ui_fixture", ROOT / "plugins/prompt-skills/scripts/check-ui.py") -fixture = importlib.util.module_from_spec(spec) -spec.loader.exec_module(fixture) -fixture.COMPILED = ROOT / "plugins/admin-observability/ui/compiled" -fixture.HARNESS = r''' -''' - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--output", required=True, type=Path) - args = parser.parse_args() - assert (fixture.COMPILED / "index.html").is_file() - args.output.mkdir(parents=True, exist_ok=True) - server = ThreadingHTTPServer(("127.0.0.1", 0), fixture.Handler) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - cases = [] - try: - with sync_playwright() as playwright: - browser = playwright.chromium.launch() - try: - for width in (1200, 390): - for theme in ("light", "dark"): - page = browser.new_page(viewport={"width": width, "height": 1000}) - errors, external = [], [] - page.on("pageerror", lambda error: errors.append(str(error))) - def offline(route): - if urlsplit(route.request.url).hostname != "127.0.0.1": - external.append(route.request.url) - route.abort() - else: - route.continue_() - page.route("**/*", offline) - page.goto(f"http://127.0.0.1:{server.server_port}/?theme={theme}") - frame = page.frame_locator("iframe") - expect(frame.locator('[data-test="tab-events"]')).to_have_attribute("aria-selected", "true") - assert frame.get_by_role("tab").all_text_contents() == ["事件", "配置"] - frame.locator('[data-test="tab-config"]').click() - frame.get_by_role("button", name="编辑", exact=True).click() - name, token = frame.get_by_label("节点名称", exact=True), frame.get_by_label("API Key", exact=True) - expect(token).to_have_value("") - name.fill("更新后的合成节点") - token.fill("synthetic-replacement-only") - page.evaluate("setPluginAvailable(false)") - expect(frame.locator("#app")).to_have_attribute("inert", "") - expect(token).to_have_value("synthetic-replacement-only") - page.evaluate("setPluginAvailable(true)") - expect(frame.locator("#app")).not_to_have_attribute("inert", "") - assert frame.locator("body").evaluate("node=>node.scrollWidth<=innerWidth") - page.screenshot(path=str(args.output / f"audit-dialog-{width}-{theme}.png"), full_page=True) - frame.locator('[data-test="save-endpoint"]').click() - frame.locator('[data-test="save-config"]').click() - page.wait_for_function("saved.length===1") - assert page.evaluate("saved[0].expected_config_version") == 7 - assert page.evaluate("saved[0].endpoints[0].token") == "synthetic-replacement-only" - frame.get_by_role("button", name="编辑", exact=True).click() - expect(token).to_have_value("") - frame.get_by_role("button", name="取消", exact=True).click() - expect(frame.get_by_role("dialog")).not_to_be_visible() - assert not errors and not external, (errors, external) - cases.append({"width": width, "theme": theme, "input_preserved": True, "version_fence": 7, "saved_token_not_echoed": True, "page_errors": errors, "external_requests": external}) - page.close() - finally: - browser.close() - finally: - server.shutdown() - server.server_close() - thread.join() - evidence = {"offline_only": True, "cases": cases, "visual_inspection": "not performed; screenshots and DOM assertions recorded separately"} - (args.output / "audit-ui-check.json").write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - print(json.dumps(evidence, ensure_ascii=False)) - - -if __name__ == "__main__": - main() diff --git a/plugins/admin-observability/ui/assets/app.js b/plugins/admin-observability/ui/assets/app.js deleted file mode 100644 index 5b3f3ad5e840..000000000000 --- a/plugins/admin-observability/ui/assets/app.js +++ /dev/null @@ -1,58 +0,0 @@ -/* global Sub2APIPluginBridge */ -(async () => { - const bridge = new Sub2APIPluginBridge() - window.addEventListener('pagehide', () => bridge.dispose(), { once: true }) - const context = await bridge.context() - document.documentElement.dataset.theme = context.theme || 'light' - const en = String(context.locale).startsWith('en') - const text = (zh, english) => en ? english : zh - const node = (tag, value = '') => { const element = document.createElement(tag); element.textContent = value; return element } - const app = document.getElementById('app') - new ResizeObserver(() => bridge.resize(document.documentElement.scrollHeight)).observe(app) - app.append(node('h2', text('账号流量观测', 'Account traffic observations'))) - app.append(node('p', text('记录实际业务转发的结果,不触发测试请求。观测数据不用于限制账号或改变计费。', 'Records real gateway attempts without running test requests. Observations do not restrict accounts or change billing.'))) - const status = node('p'); status.setAttribute('role', 'status') - if (context.mode === 'configuration' || context.mode === 'admin.settings') { - let config = await bridge.config() - const input = node('input'); input.type = 'checkbox'; input.checked = !!config.telemetry_enabled - const label = node('label'); label.append(input, document.createTextNode(text('启用账号流量观测', 'Enable account traffic observations'))) - const theme = node('input'); theme.type = 'checkbox'; theme.checked = !!config.theme_enabled - const themeLabel = node('label'); themeLabel.append(theme, document.createTextNode(text('启用平面主题', 'Enable flat theme'))) - const save = node('button', text('保存设置', 'Save settings')); save.type = 'button' - save.onclick = async () => { - save.disabled = true - try { config = await bridge.save({ ...config, telemetry_enabled: input.checked, theme_enabled: theme.checked }); input.checked = !!config.telemetry_enabled; theme.checked = !!config.theme_enabled; status.textContent = text('设置已保存', 'Settings saved') } - catch (error) { status.textContent = error.message || text('保存失败', 'Save failed') } - finally { save.disabled = false } - } - app.append(label, themeLabel, save, status) - return - } - const list = node('section'), refresh = node('button', text('刷新观测', 'Refresh observations')); refresh.type = 'button' - const load = async () => { - refresh.disabled = true - try { - const rows = await bridge.invoke('telemetry.read') - list.replaceChildren() - for (const row of rows) { - const article = node('article'); article.append(node('h3', row.protocol.toUpperCase())) - if (!row.observed_since_ms) { article.append(node('p', text('尚无观测数据', 'No observations yet'))); list.append(article); continue } - const facts = node('dl') - for (const [label, value] of [ - [text('累计开始', 'Started'), row.started], [text('已结束', 'Finished'), row.finished], - [text('尚未结束', 'Unfinished'), row.unfinished], [text('近 60 秒请求', 'Requests in last 60s'), row.requests_last_60s], - [text('成功完成率', 'Completed successfully'), row.completion_rate == null ? '—' : `${(row.completion_rate * 100).toFixed(1)}%`], - ['429', row.upstream_429], ['5xx', row.upstream_5xx], [text('取消或未完成', 'Cancelled or incomplete'), row.cancelled], - [text('其他失败', 'Other failures'), row.failed_other], [text('历史槽位峰值', 'Peak observed slots'), row.peak_in_flight] - ]) facts.append(node('dt', label), node('dd', String(value))) - article.append(facts, node('p', `${text('开始观测', 'Observed since')}: ${new Date(row.observed_since_ms).toLocaleString(en ? 'en' : 'zh-CN')}`)) - list.append(article) - } - status.textContent = rows.length ? '' : text('尚无观测数据', 'No observations yet') - } catch (error) { status.textContent = error.message || text('观测暂不可用', 'Observations unavailable') } - finally { refresh.disabled = false } - } - refresh.onclick = load - app.append(refresh, status, list) - await load() -})().catch(error => { document.getElementById('app').textContent = error.message || 'Observations unavailable' }) diff --git a/plugins/admin-observability/ui/assets/fonts/MiSans-Font-Intellectual-Property-License.pdf b/plugins/admin-observability/ui/assets/fonts/MiSans-Font-Intellectual-Property-License.pdf deleted file mode 100644 index 1fe2a6fb53f35da55fe3bf93b14ce2f37bbc2c53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79535 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 diff --git a/plugins/admin-observability/ui/assets/fonts/NOTICE.md b/plugins/admin-observability/ui/assets/fonts/NOTICE.md deleted file mode 100644 index 7596ff7ca8ce..000000000000 --- a/plugins/admin-observability/ui/assets/fonts/NOTICE.md +++ /dev/null @@ -1,14 +0,0 @@ -# MiSans - -Copyright Xiaomi. Distributed under the MiSans Font Intellectual Property License. - -Original, unmodified WOFF2 files extracted from the official [MiSans download](https://hyperos.mi.com/font-download/MiSans.zip). No subsetting, conversion or glyph modifications were performed. - -The official license is included as [MiSans-Font-Intellectual-Property-License.pdf](MiSans-Font-Intellectual-Property-License.pdf). Fonts are included in the signed admin-observability plugin and loaded with `font-display: swap` while its theme is enabled. - -## Original font checksums - -- `MiSans-Regular.woff2`: SHA-256 `d704c1a932c0bd7e8a071d276cd81c0ed0c9fecfa26ac234f4bed0559fe1cb2d` -- `MiSans-Medium.woff2`: SHA-256 `44e28ca6c2f0ca79829f192831ef87b5eec7c464f5cfb7a83467f57bb6e58114` -- `MiSans-Semibold.woff2`: SHA-256 `78227c6ec59566785c65ac0b5312328bfa2f879918f3d7403725785615a9a8f6` -- `MiSans-Bold.woff2`: SHA-256 `1c5a7515b61bc82baaa2e2c2fdae2032479fb9a99e09d4d021dc17314fc5939b` diff --git a/plugins/admin-observability/ui/assets/styles.css b/plugins/admin-observability/ui/assets/styles.css deleted file mode 100644 index 77501d54ed21..000000000000 --- a/plugins/admin-observability/ui/assets/styles.css +++ /dev/null @@ -1,16 +0,0 @@ -:root { color-scheme:light; --bg:#fff; --fg:#1f2937; --muted:#6b7280; --line:#d1d5db; } -:root[data-theme=dark] { color-scheme:dark; --bg:#111827; --fg:#e5e7eb; --muted:#9ca3af; --line:#374151; } -* { box-sizing:border-box; } -body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.6 system-ui,sans-serif; } -main { padding:20px; max-width:920px; margin:auto; overflow-wrap:anywhere; } -h2 { margin:0 0 16px; font-size:18px; } -h3 { font-size:15px; margin:8px 0; } -p { margin:8px 0; color:var(--muted); } -label { display:flex; align-items:center; gap:10px; padding:8px 0; } -button { color:inherit; background:transparent; border:1px solid var(--line); border-radius:6px; padding:8px 14px; margin:12px 0; cursor:pointer; font:inherit; } -button:disabled { opacity:.55; cursor:wait; } -article { border-top:1px solid var(--line); margin-top:12px; padding:12px 0; } -dl { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:8px 16px; } -dt { color:var(--muted); } -dd { margin:0; font-variant-numeric:tabular-nums; text-align:right; } -@media(max-width:480px) { main { padding:12px; } } diff --git a/plugins/admin-observability/ui/index.html b/plugins/admin-observability/ui/index.html deleted file mode 100644 index 697c5cb4b74b..000000000000 --- a/plugins/admin-observability/ui/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -流量观测 -
diff --git a/plugins/admin-observability/ui/src/PromptAuditView.vue b/plugins/admin-observability/ui/src/PromptAuditView.vue deleted file mode 100644 index 9e9cd014cf16..000000000000 --- a/plugins/admin-observability/ui/src/PromptAuditView.vue +++ /dev/null @@ -1,480 +0,0 @@ - - - diff --git a/plugins/admin-observability/ui/src/__tests__/api.spec.ts b/plugins/admin-observability/ui/src/__tests__/api.spec.ts deleted file mode 100644 index 1aadef65695e..000000000000 --- a/plugins/admin-observability/ui/src/__tests__/api.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { emptyEventFilters } from '../viewModel' -const call = vi.hoisted(() => vi.fn()) -vi.mock('@sub2api/plugin-ui', () => ({ resource: call })) -import api from '../api' - -describe('Prompt audit named resources', () => { - beforeEach(() => call.mockReset()) - it('uses only declared resource names for config and runtime', async () => { - await api.getConfig(); await api.getRuntime() - expect(call.mock.calls).toEqual([['prompt-audit.config'], ['prompt-audit.runtime']]) - }) - it('passes an explicitly entered probe token without reading saved credentials', async () => { - call.mockResolvedValue({ ok: true, token_applied: true }) - const result = await api.probeEndpoint({ id: 'guard-1', name: 'Guard', protocol: 'openai_compatible', base_url: 'http://127.0.0.1:8000', model: 'guard', token: 'synthetic-secret', clear_token: false, timeout_ms: 1000, input_limit: 1000, enabled: true, has_token: false, token_status: 'missing' }) - expect(call).toHaveBeenCalledWith('prompt-audit.probe', { body: { endpoint: expect.objectContaining({ token: 'synthetic-secret' }) } }) - expect(JSON.stringify(result)).not.toContain('synthetic-secret') - }) - it('retains the server-issued deletion fence', async () => { - await api.deleteEventsByFilter(emptyEventFilters(), { matched_count: 2, filter_summary: {}, snapshot_max_id: 10, filter_hash: 'a'.repeat(64), confirmation_token: 'opaque-token', expires_at: '2026-07-16T00:05:00Z' }) - expect(call).toHaveBeenCalledWith('prompt-audit.delete-by-filter', { body: expect.objectContaining({ snapshot_max_id: 10, filter_hash: 'a'.repeat(64), confirmation_token: 'opaque-token', confirm: true }) }) - }) -}) diff --git a/plugins/admin-observability/ui/src/api.ts b/plugins/admin-observability/ui/src/api.ts deleted file mode 100644 index a59b69ed4ce7..000000000000 --- a/plugins/admin-observability/ui/src/api.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { resource } from '@sub2api/plugin-ui' -import type { PromptAuditConfig, PromptAuditEvent, PromptAuditGroup, PromptAuditRuntime, PromptAuditUpdateRequest, PromptDeletePreview, PromptDeleteResult, PromptEventFilters, PromptEventPage, PromptProbeResult, PromptAuditEndpointDraft } from './types' -import { eventFilterPayload, eventQueryParams } from './viewModel' - -export const getConfig = () => resource('prompt-audit.config') -export const updateConfig = (body: PromptAuditUpdateRequest) => resource('prompt-audit.update', { body }) -export const getRuntime = () => resource('prompt-audit.runtime') -export const listEvents = (filters: PromptEventFilters, page: number, pageSize: number) => resource('prompt-audit.events', { query: { page, page_size: pageSize, ...eventQueryParams(filters) } }) -export const getEvent = (id: number) => resource('prompt-audit.event', { params: { id } }) -export const deleteEvent = (id: number) => resource('prompt-audit.delete', { params: { id } }) -export const batchDeleteEvents = (ids: number[]) => resource('prompt-audit.batch-delete', { body: { ids } }) -export const previewDelete = (filters: PromptEventFilters) => resource('prompt-audit.delete-preview', { body: eventFilterPayload(filters) }) -export const listGroups = () => resource('prompt-audit.groups') -export const deleteEventsByFilter = (filters: PromptEventFilters, preview: PromptDeletePreview) => resource('prompt-audit.delete-by-filter', { body: { - filter: eventFilterPayload(filters), snapshot_max_id: preview.snapshot_max_id, - filter_hash: preview.filter_hash, confirmation_token: preview.confirmation_token, confirm: true -} }) -export const probeEndpoint = (endpoint: PromptAuditEndpointDraft) => resource('prompt-audit.probe', { body: { endpoint: { - id: endpoint.id, name: endpoint.name, protocol: 'openai_compatible', base_url: endpoint.base_url, - model: endpoint.model, token: endpoint.token || undefined, timeout_ms: endpoint.timeout_ms, - input_limit: endpoint.input_limit, enabled: endpoint.enabled -} } }) -export default { getConfig, updateConfig, probeEndpoint, getRuntime, listEvents, getEvent, deleteEvent, batchDeleteEvents, previewDelete, deleteEventsByFilter, listGroups } diff --git a/plugins/admin-observability/ui/src/components/Pagination.vue b/plugins/admin-observability/ui/src/components/Pagination.vue deleted file mode 100644 index f3929ce77233..000000000000 --- a/plugins/admin-observability/ui/src/components/Pagination.vue +++ /dev/null @@ -1,14 +0,0 @@ - - diff --git a/plugins/admin-observability/ui/src/index.html b/plugins/admin-observability/ui/src/index.html deleted file mode 100644 index 1eded5621614..000000000000 --- a/plugins/admin-observability/ui/src/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -Prompt Audit
diff --git a/plugins/admin-observability/ui/src/locales/en.ts b/plugins/admin-observability/ui/src/locales/en.ts deleted file mode 100644 index f0798f0fa352..000000000000 --- a/plugins/admin-observability/ui/src/locales/en.ts +++ /dev/null @@ -1,288 +0,0 @@ -export default { common: { - loading: 'Loading...', - submitting: 'Submitting...', - justNow: 'just now', - peakRateTooltip: 'Peak rate: {window}', - peakRateImageNote: '; image tokens billed as tokens are also affected, per-image billing is unaffected', - save: 'Save', - saved: 'Saved successfully', - deleted: 'Deleted successfully', - cancel: 'Cancel', - delete: 'Delete', - edit: 'Edit', - create: 'Create', - apply: 'Apply', - clear: 'Clear', - creating: 'Creating...', - required: 'Required', - sending: 'Sending...', - tryAgain: 'Please try again', - retry: 'Retry', - operationFailed: 'Operation failed', - update: 'Update', - confirm: 'Confirm', - reset: 'Reset', - search: 'Search', - filter: 'Filter', - export: 'Export', - import: 'Import', - actions: 'Actions', - status: 'Status', - name: 'Name', - email: 'Email', - password: 'Password', - submit: 'Submit', - back: 'Back', - next: 'Next', - yes: 'Yes', - no: 'No', - all: 'All', - none: 'None', - selectAll: 'Select all', - noData: 'No data', - expand: 'Expand', - collapse: 'Collapse', - success: 'Success', - error: 'Error', - critical: 'Critical', - warning: 'Warning', - info: 'Info', - active: 'Active', - inactive: 'Inactive', - more: 'More', - close: 'Close', - toggleMenu: 'Toggle menu', - userMenu: 'User menu', - pageNotFound: 'Page not found', - enabled: 'Enabled', - disabled: 'Disabled', - total: 'Total', - balance: 'Balance', - availableBalance: 'Available balance', - frozenBalance: 'Frozen balance', - totalBalance: 'Total balance', - available: 'Available', - copiedToClipboard: 'Copied to clipboard', - copied: 'Copied', - copyFailed: 'Failed to copy', - verifying: 'Verifying...', - processing: 'Processing...', - contactSupport: 'Contact Support', - add: 'Add', - invalidEmail: 'Please enter a valid email address', - optional: 'optional', - selectOption: 'Select an option', - searchPlaceholder: 'Search...', - noOptionsFound: 'No options found', - noGroupsAvailable: 'No groups available', - unknownError: 'Unknown error occurred', - saving: 'Saving...', - selectedCount: '({count} selected)', - refresh: 'Refresh', - autoRefresh: { - title: 'Auto Refresh', - enable: 'Enable auto refresh', - countdown: 'Auto refresh: {seconds}s', - seconds: '{n} seconds', - }, - view: 'View', - settings: 'Settings', - chooseFile: 'Choose File', - upload: 'Upload', - remove: 'Remove', - noFileSelected: 'No file selected', - selectedFile: 'Selected: {name}', - fileReadFailed: 'Failed to read file', - selectImageFile: 'Please select an image file', - fileTooLargeKb: 'File too large ({size} KB), max {max} KB', - copy: 'Copy', - notAvailable: 'N/A', - now: 'Now', - today: 'Today', - tomorrow: 'Tomorrow', - unknown: 'Unknown', - minutes: 'min', - time: { - never: 'Never', - justNow: 'Just now', - minutesAgo: '{n}m ago', - hoursAgo: '{n}h ago', - daysAgo: '{n}d ago', - countdown: { - daysHours: '{d}d {h}h', - hoursMinutes: '{h}h {m}m', - minutes: '{m}m', - withSuffix: '{time} to lift' - } - } - }, nav: { - dashboard: 'Dashboard', - announcements: 'Announcements', - apiKeys: 'API Keys', - imageStudio: 'Image Studio', - batchImage: 'Batch Images', - usage: 'Usage', - redeem: 'Redeem', - affiliate: 'Affiliate Rebates', - affiliateManagement: 'Affiliate Rebates', - affiliateInviteRecords: 'Invite Records', - affiliateRebateRecords: 'Rebate Records', - affiliateTransferRecords: 'Transfer Records', - profile: 'Profile', - users: 'Users', - groups: 'Groups', - channels: 'Channels', - availableChannels: 'Available Channels', - modelPlaza: 'Model Plaza', - subscriptions: 'Subscriptions', - accounts: 'Accounts', - plugins: 'Plugins', - tasks: 'Tasks', - cindyAccounts: 'Cindy Accounts', - proxies: 'Proxies', - redeemCodes: 'Redeem Codes', - ops: 'Ops', - promoCodes: 'Promo Codes', - settings: 'Settings', - myAccount: 'My Account', - extensions: 'Extensions', - lightMode: 'Light Mode', - darkMode: 'Dark Mode', - collapse: 'Collapse', - expand: 'Expand', - logout: 'Logout', - github: 'GitHub', - mySubscriptions: 'My Subscriptions', - buySubscription: 'Recharge / Subscription', - recharge: 'Recharge', - subscribe: 'Subscription', - docs: 'Docs', - myOrders: 'My Orders', - orderManagement: 'Orders', - paymentDashboard: 'Payment Dashboard', - paymentConfig: 'Payment Config', - paymentPlans: 'Plans', - channelManagement: 'Channels', - channelPricing: 'Channel Pricing', - channelMonitor: 'Channel Monitor', - channelStatus: 'Channel Status', - riskControl: 'Risk Control', - securityAudit: 'Security Audit', - contentModeration: 'Content Moderation', - promptAudit: 'Prompt Audit', - auditLogs: 'Audit Logs', - systemPrompts: 'System Prompts', - }, pagination: { - showing: 'Showing', - to: 'to', - of: 'of', - results: 'results', - page: 'Page', - pageOf: 'Page {page} of {total}', - previous: 'Previous', - next: 'Next', - perPage: 'Per page', - goToPage: 'Go to page {page}', - jumpTo: 'Jump to', - jumpPlaceholder: 'Page', - jumpAction: 'Go' - }, admin: { - promptAudit: { - title: 'Prompt Audit', - description: 'Review user input asynchronously or block it synchronously through OpenAI-compatible Qwen3Guard nodes. Full prompts are stored with events for admin review.', - configVersion: 'Config version v{version}', - tabs: { config: 'Configuration', events: 'Events' }, - actions: { refresh: 'Refresh runtime', retry: 'Retry', Allow: 'Allow', Warn: 'Warn', Block: 'Block' }, - common: { actions: 'Actions', never: 'Never' }, - mode: { off: 'Off', async_audit: 'Async audit only', blocking: 'Synchronous audit and block' }, - status: { disabled: 'Disabled', running: 'Running', degraded: 'Degraded', error: 'Error', healthy: 'Healthy', failed: 'Failed', stale: 'Stale heartbeat' }, - decisions: { pass: 'Pass', flag: 'Flag', critical: 'Critical' }, - riskLevels: { low: 'Low', medium: 'Medium', high: 'High', critical: 'Critical' }, - scanners: { - violent: 'Violent', - non_violent_illegal_acts: 'Non-violent Illegal Acts', - sexual_content_or_sexual_acts: 'Sexual Content or Sexual Acts', - pii: 'PII', - suicide_and_self_harm: 'Suicide & Self-Harm', - unethical_acts: 'Unethical Acts', - politically_sensitive_topics: 'Politically Sensitive Topics', - copyright_violation: 'Copyright Violation', - jailbreak: 'Jailbreak', - }, - scannerDescriptions: { - violent: 'Violence or threats of violence', - non_violent_illegal_acts: 'Non-violent illegal activity', - sexual_content_or_sexual_acts: 'Sexual content or sexual acts', - pii: 'Personal identifying information', - suicide_and_self_harm: 'Suicide or self-harm', - unethical_acts: 'Unethical behavior', - politically_sensitive_topics: 'Politically sensitive topics', - copyright_violation: 'Copyright infringement', - jailbreak: 'Prompt injection or jailbreak attempt', - }, - runtime: { - title: 'Runtime overview', - description: 'Shows the configuration currently active on the server. Unsaved draft changes do not affect these values.', - process: 'Process status', mode: 'Effective mode', version: 'Active / expected version', workers: 'Active / total workers', - queue: 'Active jobs / capacity', dependencies: 'Dependencies', guardMetrics: 'Synchronous Guard metrics', latest: 'Latest processing and error', - queueBreakdown: 'queued {queued} · processing {processing} · retry {retry} · done {done} · failed {failed}', - deliveryTotals: 'Total enqueued {enqueued} · dropped {dropped} · processed {processed} · failed {failed}', - }, - metrics: { total: 'Total', allowed: 'Allowed', flagged: 'Flagged', blocked: 'Blocked', unavailable: 'Unavailable', timeouts: 'Timeouts', failovers: 'Failovers' }, - pool: { - title: 'Audit pool', description: 'Enabled OpenAI-compatible nodes are tried in order. Probes run from the server network.', - add: 'Add node', edit: 'Edit node', empty: 'No audit nodes configured.', node: 'Node', model: 'Model', limits: 'Timeout / chunk limit', credential: 'Credential and probe', - configured: 'API Key configured', missing: 'API Key missing', invalid: 'API Key cannot be decrypted; re-enter it', probe: 'Test connection', probing: 'Probing…', - probeProgress: 'Config validated ✓ · request sent · awaiting service response…', probeResult: 'Config ✓ · request ✓ · HTTP {http} · {status} · {latency} ms', - name: 'Node name', id: 'Stable node ID', baseUrl: 'Base URL', apiKey: 'API Key', keepSecret: 'Leave blank to keep the saved API Key', reenterSecret: 'The saved API Key cannot be decrypted (encryption key changed); enter a new one', - secretHint: 'Plaintext exists only in this editor and is cleared immediately after a successful save.', clearSecret: 'Explicitly clear the saved API Key', timeout: 'Total timeout (ms)', inputLimit: 'Unicode characters per chunk', - toggleNode: 'Toggle node {name}', deleteConfirm: 'Remove “{name}” from the draft? It takes effect after saving.', - }, - policy: { - title: 'Audit policy', description: 'Configure group scope, nine input-risk categories, workers, and queue bounds.', scope: 'Scope', allGroups: 'All groups', selectedGroups: 'Selected groups', - searchGroups: 'Search groups', noGroups: 'No matching groups', missingGroups: 'Configured IDs for groups that no longer exist', selectedCount: '{count} groups selected', - scanners: 'Qwen3Guard input-risk categories', workerCount: 'Worker count', queueCapacity: 'Persistent queue capacity', strategy: 'Node strategy', strategyHint: 'Try nodes in configuration order and fail over when allowed.', - }, - saveBar: { enabled: 'Enable prompt audit', blocking: 'Synchronous blocking', blockingLatestTurnOnly: 'Only latest input and prior output', storePass: 'Store safe events', dirty: 'Unsaved changes', synced: 'Configuration synced' }, - blockingConfirm: { - title: 'Enable synchronous blocking?', - message: 'Applicable requests wait for Guard before account selection, billing, or upstream access. Block, unavailable Guard, and invalid responses all prevent upstream access.', - confirm: 'I understand; enable it', - }, - events: { - title: 'Audit events', description: 'Review events by identity, route, risk, hash, and time; the detail view shows the full prompt.', decision: 'Decision', risk: 'Risk level', endpoint: 'Endpoint', groupId: 'Group ID', userId: 'User ID', apiKeyId: 'API Key ID', keyword: 'Keyword', - startAt: 'Start time', endAt: 'End time', deleteSelected: 'Delete selected ({count})', deleteByFilter: 'Delete by filter', - filterDeleteDialogTitle: 'Delete audit events by filter', filterDeleteDialogDesc: 'Choose the time range and risk criteria, then delete directly. Deletion is permanent. Generate a preview first if you want to see the match count.', - filterTimeRange: 'Deletion time range', filterTimeRangeHint: 'Deletes events created before the selected cutoff. Events created after the preview are not affected.', - timePresets: { '1d': 'Older than 1 day', '7d': 'Older than 7 days', '30d': 'Older than 30 days', '90d': 'Older than 90 days', all: 'All time', custom: 'Custom range' }, - customRangeInvalid: 'A custom range needs a valid start and end time, with the start before the end.', - moreConditions: 'More conditions (endpoint / keyword / group / user)', - filterDeletePreviewAction: 'Generate delete preview', filterDeletePreviewing: 'Generating preview…', filterDeleteNeedPreview: 'You can delete directly, or generate a preview first to see the match count.', - filterDeleteConfirmInvalidRange: 'Select a valid deletion time range first (a custom range needs a start before the end).', filterDeleteConfirmNoMatches: 'The current filters matched 0 events, so there is nothing to delete.', - selectAll: 'Select all events on this page', selectEvent: 'Select event {id}', time: 'Time', identity: 'User / email / API Key', user: 'Username', email: 'User email', apiKey: 'API Key name', group: 'Group', route: 'Endpoint / model', result: 'Decision / risk', preview: 'Redacted preview', empty: 'No matching events.', - passEventsDisabled: '“Store safe events” is off. Safe requests are still audited but do not appear in this list; Flag and Critical risk events are still stored.', openConfiguration: 'Open configuration', - detailTitle: 'Prompt audit event details', tabs: { summary: 'Audit summary', risks: 'Specific risks', technical: 'Technical details' }, - promptFull: 'Full prompt (unredacted)', - promptFullHint: 'The full prompt is stored with this event for admin review only. Treat it as sensitive data and do not share it.', - guardReturn: 'Model audit return', - guardReturnHint: 'Normalized Guard result (decision, categories, scores, and redacted evidence). Raw response bodies are not stored.', - riskSummaries: 'Risk summaries', - evidence: 'Redacted evidence', - score: 'Score', - categories: 'Categories', model: 'Model', stage: 'Request stage', noRisks: 'No derived risk summaries for this event.', - requestId: 'Request ID', promptHash: 'Prompt SHA-256', - technical: { - scanner: 'Scanner', policy: 'Policy', guardEndpoint: 'Guard endpoint', config: 'Config', - chunks: 'Chunks', latency: 'Latency', protocol: 'Protocol', - }, - deleteConfirmTitle: 'Delete audit events?', deleteConfirmMessage: 'This permanently deletes {count} events and eligible orphan jobs.', filterDeleteCount: 'The server snapshot matches {count} events.', snapshotMax: 'Snapshot maximum event ID', expiresAt: 'Confirmation token expires', filterDeleteWarning: 'Only events at or below the preview high-water mark are deleted. Newer events survive. Any filter change requires a new preview.', confirmFilterDelete: 'Permanently delete', - }, - messages: { saved: 'Prompt Audit configuration saved; plaintext API Key state was cleared.', probeSucceeded: 'The audit node is reachable.', deleted: 'Deleted {count} audit events.' }, - errors: { - loadConfig: 'Unable to load Prompt Audit configuration.', loadRuntime: 'Unable to load Prompt Audit runtime.', loadGroups: 'Unable to load groups.', loadEvents: 'Unable to load audit events.', loadDetail: 'Unable to load event details.', saveConfig: 'Unable to save the configuration.', probe: 'Node probe failed.', delete: 'Unable to delete events.', previewDelete: 'Unable to create a deletion preview. Check the time range.', deleteConfirmation: 'The deletion confirmation is invalid or expired. Preview again.', - prompt_audit_config_conflict: 'Another administrator updated this configuration. Reload the server version before deciding how to merge your draft.', - prompt_audit_encryption_key_required: 'No fixed encryption key is configured, so audit node API Keys would be lost on restart. Set the TOTP_ENCRYPTION_KEY environment variable and restart the service first.', - prompt_guard_requires_audit_enabled: 'Enable Prompt Audit before synchronous blocking.', prompt_audit_invalid_endpoint: 'The audit node configuration is invalid.', prompt_audit_endpoint_required: 'Enable at least one audit node before enabling Prompt Audit.', prompt_audit_groups_required: 'Select at least one group in selected-group mode.', prompt_audit_scanners_required: 'Enable at least one risk category.', - }, - }, -} } diff --git a/plugins/admin-observability/ui/src/locales/zh.ts b/plugins/admin-observability/ui/src/locales/zh.ts deleted file mode 100644 index 17c15cf47993..000000000000 --- a/plugins/admin-observability/ui/src/locales/zh.ts +++ /dev/null @@ -1,288 +0,0 @@ -export default { common: { - loading: '加载中...', - submitting: '提交中...', - justNow: '刚刚', - peakRateTooltip: '高峰倍率:{window}', - peakRateImageNote: ';token 计费的图片 token 同样适用,图片按次计费不受高峰影响', - save: '保存', - saved: '保存成功', - deleted: '删除成功', - cancel: '取消', - delete: '删除', - edit: '编辑', - create: '创建', - apply: '应用', - clear: '清除', - creating: '创建中...', - required: '必填', - sending: '发送中...', - tryAgain: '请重试', - retry: '重试', - operationFailed: '操作失败', - update: '更新', - confirm: '确认', - reset: '重置', - search: '搜索', - filter: '筛选', - export: '导出', - import: '导入', - actions: '操作', - status: '状态', - name: '名称', - email: '邮箱', - password: '密码', - submit: '提交', - back: '返回', - next: '下一步', - yes: '是', - no: '否', - all: '全部', - none: '无', - selectAll: '全选', - noData: '暂无数据', - expand: '展开', - collapse: '收起', - success: '成功', - error: '错误', - critical: '严重', - warning: '警告', - info: '提示', - active: '启用', - inactive: '禁用', - more: '更多', - close: '关闭', - toggleMenu: '切换菜单', - userMenu: '用户菜单', - pageNotFound: '页面不存在', - enabled: '已启用', - disabled: '已禁用', - total: '总计', - balance: '余额', - availableBalance: '可用余额', - frozenBalance: '冻结金额', - totalBalance: '总余额', - available: '可用', - copiedToClipboard: '已复制到剪贴板', - copied: '已复制', - copyFailed: '复制失败', - verifying: '验证中...', - processing: '处理中...', - contactSupport: '联系客服', - add: '添加', - invalidEmail: '请输入有效的邮箱地址', - optional: '可选', - selectOption: '请选择', - searchPlaceholder: '搜索...', - noOptionsFound: '无匹配选项', - noGroupsAvailable: '无可用分组', - unknownError: '发生未知错误', - saving: '保存中...', - selectedCount: '(已选 {count} 个)', - refresh: '刷新', - autoRefresh: { - title: '自动刷新', - enable: '启用自动刷新', - countdown: '自动刷新: {seconds}s', - seconds: '{n} 秒', - }, - view: '查看', - settings: '设置', - chooseFile: '选择文件', - upload: '上传', - remove: '移除', - noFileSelected: '未选择文件', - selectedFile: '已选:{name}', - fileReadFailed: '读取文件失败', - selectImageFile: '请选择图片文件', - fileTooLargeKb: '文件过大({size} KB),上限 {max} KB', - copy: '复制', - notAvailable: '不可用', - now: '现在', - today: '今天', - tomorrow: '明天', - unknown: '未知', - minutes: '分钟', - time: { - never: '从未', - justNow: '刚刚', - minutesAgo: '{n}分钟前', - hoursAgo: '{n}小时前', - daysAgo: '{n}天前', - countdown: { - daysHours: '{d}d {h}h', - hoursMinutes: '{h}h {m}m', - minutes: '{m}m', - withSuffix: '{time} 后解除' - } - } - }, nav: { - dashboard: '仪表盘', - announcements: '公告', - apiKeys: 'API 密钥', - imageStudio: '生图工作台', - batchImage: '批量生图', - usage: '使用记录', - redeem: '兑换', - affiliate: '邀请返利', - affiliateManagement: '邀请返利', - affiliateInviteRecords: '邀请记录', - affiliateRebateRecords: '返利记录', - affiliateTransferRecords: '提取记录', - profile: '个人资料', - users: '用户管理', - groups: '分组管理', - channels: '渠道管理', - availableChannels: '可用渠道', - modelPlaza: '模型广场', - subscriptions: '订阅管理', - accounts: '账号管理', - plugins: '插件管理', - tasks: '任务', - cindyAccounts: 'Cindy 账号', - proxies: 'IP管理', - redeemCodes: '兑换码', - ops: '运维监控', - promoCodes: '优惠码', - settings: '系统设置', - myAccount: '我的账户', - extensions: '扩展功能', - lightMode: '浅色模式', - darkMode: '深色模式', - collapse: '收起', - expand: '展开', - logout: '退出登录', - github: 'GitHub', - mySubscriptions: '我的订阅', - buySubscription: '充值/订阅', - recharge: '充值', - subscribe: '订阅', - docs: '文档', - myOrders: '我的订单', - orderManagement: '订单管理', - paymentDashboard: '支付概览', - paymentConfig: '支付配置', - paymentPlans: '订阅套餐', - channelManagement: '渠道管理', - channelPricing: '渠道定价', - channelMonitor: '渠道监控', - channelStatus: '渠道状态', - riskControl: '风控中心', - securityAudit: '安全审计', - contentModeration: '内容审核', - promptAudit: '提示词审计', - auditLogs: '操作日志', - systemPrompts: '系统提示词', - }, pagination: { - showing: '显示', - to: '至', - of: '共', - results: '条结果', - page: '页', - pageOf: '第 {page} / {total} 页', - previous: '上一页', - next: '下一页', - perPage: '每页', - goToPage: '跳转到第 {page} 页', - jumpTo: '跳转页', - jumpPlaceholder: '页码', - jumpAction: '跳转' - }, admin: { - promptAudit: { - title: '提示词审计', - description: '通过 OpenAI 兼容 Qwen3Guard 节点异步复核或同步阻止用户输入;事件的完整提示词会入库保存,仅供管理员复核。', - configVersion: '配置版本 v{version}', - tabs: { config: '配置', events: '事件' }, - actions: { refresh: '刷新运行态', retry: '重试', Allow: '放行', Warn: '警告', Block: '阻止' }, - common: { actions: '操作', never: '从未' }, - mode: { off: '已关闭', async_audit: '异步只审计', blocking: '同步审计并阻止' }, - status: { disabled: '未启用', running: '运行中', degraded: '降级', error: '错误', healthy: '健康', failed: '失败', stale: '心跳过期' }, - decisions: { pass: '通过', flag: '标记', critical: '严重' }, - riskLevels: { low: '低', medium: '中', high: '高', critical: '严重' }, - scanners: { - violent: '暴力', - non_violent_illegal_acts: '非暴力违法行为', - sexual_content_or_sexual_acts: '色情内容或性行为', - pii: '个人身份信息', - suicide_and_self_harm: '自杀与自残', - unethical_acts: '不道德行为', - politically_sensitive_topics: '政治敏感话题', - copyright_violation: '版权侵犯', - jailbreak: '越狱', - }, - scannerDescriptions: { - violent: '暴力或暴力威胁', - non_violent_illegal_acts: '非暴力违法活动', - sexual_content_or_sexual_acts: '色情内容或性行为', - pii: '个人身份信息', - suicide_and_self_harm: '自杀或自残', - unethical_acts: '不道德行为', - politically_sensitive_topics: '政治敏感话题', - copyright_violation: '版权侵权', - jailbreak: '提示注入或越狱尝试', - }, - runtime: { - title: '运行概览', - description: '显示服务端当前生效状态;未保存的草稿不会改变这些数值。', - process: '进程状态', mode: '生效模式', version: '生效 / 期望版本', workers: '活动 / 总 Worker', - queue: '活动任务 / 容量', dependencies: '依赖', guardMetrics: '同步 Guard 指标', latest: '最近处理与错误', - queueBreakdown: 'queued {queued} · processing {processing} · retry {retry} · done {done} · failed {failed}', - deliveryTotals: '累计入队 {enqueued} · 丢弃 {dropped} · 处理 {processed} · 失败 {failed}', - }, - metrics: { total: '总计', allowed: '放行', flagged: '标记', blocked: '阻止', unavailable: '不可用', timeouts: '超时', failovers: '故障切换' }, - pool: { - title: '审计池', description: '按顺序使用启用的 OpenAI 兼容节点;探测由服务端真实网络环境发起。', - add: '新增节点', edit: '编辑节点', empty: '尚未配置审计节点。', node: '节点', model: '模型', limits: '超时 / 单片上限', credential: '凭据与探测', - configured: 'API Key 已配置', missing: '未配置 API Key', invalid: 'API Key 无法解密,请重新输入', probe: '连接测试', probing: '探测中…', - probeProgress: '配置校验 ✓ · 请求已发送 · 等待服务响应…', probeResult: '配置校验 ✓ · 请求 ✓ · HTTP {http} · {status} · {latency} ms', - name: '节点名称', id: '稳定节点 ID', baseUrl: 'Base URL', apiKey: 'API Key', keepSecret: '留空以保留已保存的 API Key', reenterSecret: '已保存的 API Key 无法解密(加密密钥已变更),请重新输入', - secretHint: '明文只在本次编辑内存中存在;保存成功后会立即清除。', clearSecret: '显式清除已保存的 API Key', timeout: '总超时(毫秒)', inputLimit: '单片 Unicode 字符上限', - toggleNode: '切换节点 {name}', deleteConfirm: '从草稿中删除节点“{name}”?保存配置后生效。', - }, - policy: { - title: '审计策略', description: '配置适用分组、九类输入风险、Worker 与队列边界。', scope: '适用范围', allGroups: '全部分组', selectedGroups: '指定分组', - searchGroups: '搜索分组', noGroups: '没有匹配分组', missingGroups: '配置中包含已删除的分组 ID', selectedCount: '已选择 {count} 个分组', - scanners: 'Qwen3Guard 输入风险分类', workerCount: 'Worker 数量', queueCapacity: '持久队列容量', strategy: '节点策略', strategyHint: '按配置顺序优先尝试,必要时故障切换。', - }, - saveBar: { enabled: '启用提示词审计', blocking: '同步阻止', blockingLatestTurnOnly: '仅审最新输入和上一轮输出', storePass: '保存安全事件', dirty: '有未保存的更改', synced: '配置已同步' }, - blockingConfirm: { - title: '开启同步阻止?', - message: '适用请求会在账号选择、计费和访问上游之前等待 Guard。命中 Block、Guard 不可用或响应非法时,请求都不会访问上游。', - confirm: '理解风险并开启', - }, - events: { - title: '审计事件', description: '按身份、入口、风险、Hash 和时间复核事件,详情中可查看完整提示词。', decision: '判定', risk: '风险等级', endpoint: '入口', groupId: '分组 ID', userId: '用户 ID', apiKeyId: 'API Key ID', keyword: '关键词', - startAt: '开始时间', endAt: '结束时间', deleteSelected: '删除选中项({count})', deleteByFilter: '按筛选删除', - filterDeleteDialogTitle: '按筛选删除审计事件', filterDeleteDialogDesc: '选择删除的时间范围与风险条件后即可执行删除;删除不可恢复。如需提前查看匹配数量,可先获取删除预览。', - filterTimeRange: '删除时间范围', filterTimeRangeHint: '将删除所选截止时间之前产生的事件;预览后新产生的事件不受影响。', - timePresets: { '1d': '1 天前', '7d': '7 天前', '30d': '30 天前', '90d': '90 天前', all: '全部时间', custom: '自定义范围' }, - customRangeInvalid: '自定义范围需要有效的开始与结束时间,且开始早于结束。', - moreConditions: '更多条件(入口 / 关键词 / 分组 / 用户)', - filterDeletePreviewAction: '获取删除预览', filterDeletePreviewing: '正在生成预览…', filterDeleteNeedPreview: '可直接确认删除;如需提前查看匹配数量,可先获取删除预览。', - filterDeleteConfirmInvalidRange: '请先选择有效的删除时间范围(自定义范围需开始早于结束)。', filterDeleteConfirmNoMatches: '当前筛选匹配 0 条事件,没有可删除的内容。', - selectAll: '选择当前页全部事件', selectEvent: '选择事件 {id}', time: '时间', identity: '用户 / 邮箱 / API Key', user: '用户名', email: '用户邮箱', apiKey: 'API Key 名称', group: '分组', route: '入口 / 模型', result: '判定 / 风险', preview: '脱敏预览', empty: '没有符合条件的事件。', - passEventsDisabled: '当前未开启“保存安全事件”:安全请求仍会完成审计,但不会出现在事件列表中;Flag 和 Critical 风险事件仍会保存。', openConfiguration: '前往配置', - detailTitle: '提示词审计事件详情', tabs: { summary: '审计摘要', risks: '具体风险', technical: '技术信息' }, - promptFull: '完整提示词(未脱敏)', - promptFullHint: '完整提示词已随事件入库,仅供管理员复核触发内容;请按敏感数据妥善处理,切勿外泄。', - guardReturn: '模型审计返回', - guardReturnHint: '展示 Guard 归一化后的结构化结果(判定、分类、分数与脱敏证据),不含原始响应体。', - riskSummaries: '风险摘要', - evidence: '脱敏证据', - score: '分数', - categories: '分类', model: '模型', stage: '请求阶段', noRisks: '本事件没有派生风险摘要。', - requestId: 'Request ID', promptHash: 'Prompt SHA-256', - technical: { - scanner: '扫描器', policy: '策略', guardEndpoint: 'Guard 节点', config: '配置版本', - chunks: '分片数', latency: '耗时', protocol: '协议', - }, - deleteConfirmTitle: '删除审计事件?', deleteConfirmMessage: '将永久删除 {count} 条事件及符合条件的孤立任务。', filterDeleteCount: '服务端快照匹配 {count} 条事件。', snapshotMax: '快照最大事件 ID', expiresAt: '确认令牌过期时间', filterDeleteWarning: '只删除预览高水位内的事件;预览后产生的新事件会保留。筛选一旦变化,必须重新预览。', confirmFilterDelete: '确认永久删除', - }, - messages: { saved: '提示词审计配置已保存,明文 API Key 状态已清除。', probeSucceeded: '审计节点连接正常。', deleted: '已删除 {count} 条审计事件。' }, - errors: { - loadConfig: '无法加载提示词审计配置。', loadRuntime: '无法加载提示词审计运行态。', loadGroups: '无法加载分组列表。', loadEvents: '无法加载审计事件。', loadDetail: '无法加载事件详情。', saveConfig: '配置保存失败。', probe: '节点探测失败。', delete: '事件删除失败。', previewDelete: '无法生成删除预览,请检查时间范围。', deleteConfirmation: '删除确认无效或已过期,请重新预览。', - prompt_audit_config_conflict: '配置已被其他管理员更新。请重新加载服务端配置,再决定如何合并本地草稿。', - prompt_audit_encryption_key_required: '未配置固定加密密钥,审计节点 API Key 将在服务重启后失效。请先设置 TOTP_ENCRYPTION_KEY 环境变量并重启服务。', - prompt_guard_requires_audit_enabled: '开启同步阻止前必须先启用提示词审计。', prompt_audit_invalid_endpoint: '审计节点配置无效。', prompt_audit_endpoint_required: '启用审计前至少需要一个启用节点。', prompt_audit_groups_required: '指定分组模式至少需要选择一个分组。', prompt_audit_scanners_required: '至少需要启用一个风险分类。', - }, - }, -} } diff --git a/plugins/admin-observability/ui/src/main.ts b/plugins/admin-observability/ui/src/main.ts deleted file mode 100644 index 97265a46883d..000000000000 --- a/plugins/admin-observability/ui/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mountPlugin } from '@sub2api/plugin-ui' -import '@sub2api/plugin-ui/base.css' -import App from './PromptAuditView.vue' -import zh from './locales/zh' -import en from './locales/en' -void mountPlugin(App, { zh, en }).catch(error => { document.getElementById('app')!.textContent = error instanceof Error ? error.message : 'Plugin unavailable' }) diff --git a/plugins/bundle.source.json b/plugins/bundle.source.json index a348ecf58bb9..04ab5f0c4e9f 100644 --- a/plugins/bundle.source.json +++ b/plugins/bundle.source.json @@ -4,7 +4,6 @@ { "directory": "codex-runtime", "default_enabled": true, "migration": "codex-tickets-v1" }, { "directory": "prompt-skills", "default_enabled": true }, { "directory": "account-tools", "default_enabled": true }, - { "directory": "cindy-provider", "default_enabled": true, "migration": "cindy-provider-v1" }, - { "directory": "admin-observability", "default_enabled": true, "migration": "admin-observability-v1" } + { "directory": "cindy-provider", "default_enabled": true, "migration": "cindy-provider-v1" } ] } From b821bdbfa84be4903296c070d1b17f8883534759 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 12:02:33 +0800 Subject: [PATCH 04/13] refactor(deplugin): compile prompt-skills into the host The prompt application and skill registry policy moves from the prompt-skills plugin process into backend/internal/promptskills (policy, registry, source, including the embedded skill seed). The host calls it in process through invokePromptSkills; the account-scoped plan path, prompt management, source sync, seed loading and availability checks no longer go through the plugin manager, and the source fetch and skill sync lose their plugin execution binding. The 27 system prompt routes drop their plugin resource admission. The management page, advanced drawer, rules manager and account binding panel become native views again (views/admin/SystemPromptsView.vue, components/admin/systemPrompt, AccountPromptBindingPanel.vue). They use the existing admin system prompts API module and a new systemPromptRules API, and their messages return to the host locale modules removed in fe8973188. The account action menu and bulk bar get a native "Account prompts" entry for the same OpenAI/Cindy accounts the plugin action accepted. The read-only plugin settings card that only showed the two fixed size limits is not recreated. Tests that asserted plugin rollout or lifecycle semantics are removed; the account failover, carrier restore and registry-profile limit tests now drive per-account decisions through invokePromptSkills. The .gitattributes LF rules and the release/verify workflow paths follow the seed to its new location, and the seed scripts directory keeps its .gitignore exception. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 10 +- .github/scripts/test_first_party_plugins.sh | 3 +- .github/workflows/downstream-release.yml | 2 +- .github/workflows/downstream-verify.yml | 12 +- .gitignore | 2 + backend/go.mod | 3 - .../promptskills}/policy/management.go | 2 +- .../internal/promptskills}/policy/module.go | 4 +- .../promptskills}/policy/module_test.go | 0 .../internal/promptskills}/policy/rules.go | 0 .../promptskills}/policy/rules_test.go | 0 .../promptskills}/registry/controller.go | 0 .../promptskills}/registry/manifest.go | 0 .../codexrip_reverse_skill_system_prompt.txt | 0 .../registry/publication_test.go | 0 .../promptskills}/registry/references.go | 0 .../promptskills}/registry/seed/manifest.json | 0 .../registry/seed/pinned/waf-bypass.md | 0 .../registry/seed/tree/CHANGELOG.md | 0 .../promptskills}/registry/seed/tree/LICENSE | 0 .../registry/seed/tree/NOTICE.md | 0 .../registry/seed/tree/README_AI.md | 0 .../promptskills}/registry/seed/tree/RULES.md | 0 .../promptskills}/registry/seed/tree/SKILL.md | 0 .../registry/seed/tree/agents/openai.yaml | 0 .../assets/templates/cloud-handoff-prompt.md | 0 .../seed/tree/assets/templates/ctf-writeup.md | 0 .../tree/assets/templates/mission-state.md | 0 .../assets/templates/research-result.json | 0 .../tree/assets/templates/writeups-index.md | 0 .../seed/tree/ctf-orchestrator/LICENSE | 0 .../seed/tree/ctf-orchestrator/README.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/ad-certificate-abuse.md | 0 .../competition-agent-cloud/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/agent-cloud.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/android-hooking.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/browser-persistence.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/bundle-sourcemap-recovery.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/cloud-metadata-path.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/container-runtime.md | 0 .../competition-crypto-mobile/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/crypto-mobile.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/custom-protocol-replay.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/dpapi-credential-chain.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/file-parser-chain.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/firmware-layout.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/forensic-timeline.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/graphql-rpc-drift.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/identity-windows.md | 0 .../competition-ios-runtime/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/ios-runtime.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/jwt-claim-confusion.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/k8s-control-plane.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/kerberos-delegation.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/kernel-container-escape.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/linux-credential-pivot.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/lsass-ticket-material.md | 0 .../competition-mailbox-abuse/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/mailbox-abuse.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/malware-config.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/oauth-oidc-chain.md | 0 .../competition-pcap-protocol/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/pcap-protocol.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/prompt-injection.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/queue-worker-drift.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/race-condition-state-drift.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/relay-coercion-chain.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../request-normalization-smuggling.md | 0 .../competition-reverse-pwn/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/reverse-pwn.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/runtime-routing.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/ssrf-metadata-pivot.md | 0 .../competition-stego-media/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/stego-media.md | 0 .../competition-supply-chain/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/supply-chain.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/template-render-path.md | 0 .../competition-web-runtime/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../cookie-hmac-key-reuse-auth-bypass.md | 0 .../references/routing-runtime.md | 0 .../INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/websocket-runtime.md | 0 .../competition-windows-pivot/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/windows-pivot.md | 0 .../ctf-sandbox-orchestrator/INSTRUCTIONS.md | 0 .../agents/openai.yaml | 0 .../references/agent-cloud.md | 0 .../references/crypto-mobile.md | 0 .../references/ctf-resources-digest.md | 0 .../references/identity-windows.md | 0 .../references/reporting.md | 0 .../references/reverse-native.md | 0 .../references/router-matrix.md | 0 .../references/web-api.md | 0 .../registry/seed/tree/evals/evals.json | 0 .../seed/tree/evals/files/decoy-flags.txt | 0 .../tree/evals/files/redaction-sample.har | 0 .../runtime-conflict/capture-summary.json | 0 .../files/runtime-conflict/source-note.md | 0 .../seed/tree/evals/files/safe_handler.py | 0 .../evals/files/untrusted-agent-document.txt | 0 .../seed/tree/evals/trigger-evals.json | 0 .../seed/tree/references/ai-security.md | 0 .../references/ctf/ai-ml/adversarial-ml.md | 0 .../seed/tree/references/ctf/ai-ml/index.md | 0 .../tree/references/ctf/ai-ml/llm-attacks.md | 0 .../references/ctf/ai-ml/model-attacks.md | 0 .../references/ctf/crypto/advanced-math.md | 0 .../references/ctf/crypto/classic-ciphers.md | 0 .../tree/references/ctf/crypto/ecc-attacks.md | 0 .../references/ctf/crypto/exotic-crypto-2.md | 0 .../references/ctf/crypto/exotic-crypto.md | 0 .../tree/references/ctf/crypto/historical.md | 0 .../seed/tree/references/ctf/crypto/index.md | 0 .../references/ctf/crypto/lattice-and-lwe.md | 0 .../references/ctf/crypto/modern-ciphers-2.md | 0 .../references/ctf/crypto/modern-ciphers-3.md | 0 .../references/ctf/crypto/modern-ciphers.md | 0 .../references/ctf/crypto/prng-attacks.md | 0 .../seed/tree/references/ctf/crypto/prng.md | 0 .../references/ctf/crypto/rsa-attacks-2.md | 0 .../tree/references/ctf/crypto/rsa-attacks.md | 0 .../references/ctf/crypto/stream-ciphers.md | 0 .../references/ctf/crypto/zkp-and-advanced.md | 0 .../references/ctf/forensics/3d-printing.md | 0 .../references/ctf/forensics/disk-advanced.md | 0 .../ctf/forensics/disk-and-memory.md | 0 .../references/ctf/forensics/disk-recovery.md | 0 .../tree/references/ctf/forensics/index.md | 0 .../ctf/forensics/linux-forensics.md | 0 .../ctf/forensics/network-advanced.md | 0 .../tree/references/ctf/forensics/network.md | 0 .../ctf/forensics/peripheral-capture.md | 0 .../ctf/forensics/signals-and-hardware.md | 0 .../references/ctf/forensics/steganography.md | 0 .../ctf/forensics/stego-advanced-2.md | 0 .../ctf/forensics/stego-advanced.md | 0 .../references/ctf/forensics/stego-image.md | 0 .../tree/references/ctf/forensics/windows.md | 0 .../ctf/malware/c2-and-protocols.md | 0 .../seed/tree/references/ctf/malware/index.md | 0 .../references/ctf/malware/pe-and-dotnet.md | 0 .../ctf/malware/scripts-and-obfuscation.md | 0 .../tree/references/ctf/misc/bashjails.md | 0 .../references/ctf/misc/ctfd-navigation.md | 0 .../seed/tree/references/ctf/misc/dns.md | 0 .../references/ctf/misc/encodings-advanced.md | 0 .../tree/references/ctf/misc/encodings.md | 0 .../references/ctf/misc/games-and-vms-2.md | 0 .../references/ctf/misc/games-and-vms-3.md | 0 .../references/ctf/misc/games-and-vms-4.md | 0 .../tree/references/ctf/misc/games-and-vms.md | 0 .../seed/tree/references/ctf/misc/index.md | 0 .../tree/references/ctf/misc/linux-privesc.md | 0 .../seed/tree/references/ctf/misc/pyjails.md | 0 .../seed/tree/references/ctf/misc/rf-sdr.md | 0 .../ctf/osint/geolocation-and-media.md | 0 .../seed/tree/references/ctf/osint/index.md | 0 .../tree/references/ctf/osint/social-media.md | 0 .../tree/references/ctf/osint/web-and-dns.md | 0 .../references/ctf/pwn/advanced-exploits-2.md | 0 .../references/ctf/pwn/advanced-exploits-3.md | 0 .../references/ctf/pwn/advanced-exploits-4.md | 0 .../references/ctf/pwn/advanced-exploits-5.md | 0 .../references/ctf/pwn/advanced-exploits.md | 0 .../seed/tree/references/ctf/pwn/advanced.md | 0 .../tree/references/ctf/pwn/field-notes.md | 0 .../tree/references/ctf/pwn/format-string.md | 0 .../seed/tree/references/ctf/pwn/heap-fsop.md | 0 .../references/ctf/pwn/heap-techniques-2.md | 0 .../references/ctf/pwn/heap-techniques.md | 0 .../seed/tree/references/ctf/pwn/index.md | 0 .../tree/references/ctf/pwn/kernel-bypass.md | 0 .../references/ctf/pwn/kernel-techniques.md | 0 .../seed/tree/references/ctf/pwn/kernel.md | 0 .../references/ctf/pwn/overflow-basics.md | 0 .../tree/references/ctf/pwn/rop-advanced.md | 0 .../references/ctf/pwn/rop-and-shellcode.md | 0 .../tree/references/ctf/pwn/sandbox-escape.md | 0 .../ctf/reverse/anti-analysis-ctf.md | 0 .../references/ctf/reverse/anti-analysis.md | 0 .../references/ctf/reverse/field-notes.md | 0 .../seed/tree/references/ctf/reverse/index.md | 0 .../ctf/reverse/languages-compiled.md | 0 .../ctf/reverse/languages-platforms.md | 0 .../tree/references/ctf/reverse/languages.md | 0 .../references/ctf/reverse/patterns-ctf-2.md | 0 .../references/ctf/reverse/patterns-ctf-3.md | 0 .../references/ctf/reverse/patterns-ctf.md | 0 .../ctf/reverse/patterns-runtime.md | 0 .../tree/references/ctf/reverse/patterns.md | 0 .../ctf/reverse/platforms-hardware.md | 0 .../tree/references/ctf/reverse/platforms.md | 0 .../ctf/reverse/tools-advanced-2.md | 0 .../references/ctf/reverse/tools-advanced.md | 0 .../references/ctf/reverse/tools-dynamic.md | 0 .../references/ctf/reverse/tools-emulation.md | 0 .../seed/tree/references/ctf/reverse/tools.md | 0 .../references/ctf/web/auth-and-access-2.md | 0 .../references/ctf/web/auth-and-access.md | 0 .../tree/references/ctf/web/auth-infra.md | 0 .../seed/tree/references/ctf/web/auth-jwt.md | 0 .../ctf/web/client-side-advanced.md | 0 .../tree/references/ctf/web/client-side.md | 0 .../seed/tree/references/ctf/web/cves.md | 0 .../tree/references/ctf/web/field-notes.md | 0 .../seed/tree/references/ctf/web/index.md | 0 .../references/ctf/web/node-and-prototype.md | 0 .../tree/references/ctf/web/server-side-2.md | 0 .../ctf/web/server-side-advanced-2.md | 0 .../ctf/web/server-side-advanced-3.md | 0 .../ctf/web/server-side-advanced-4.md | 0 .../ctf/web/server-side-advanced.md | 0 .../references/ctf/web/server-side-deser.md | 0 .../references/ctf/web/server-side-exec-2.md | 0 .../references/ctf/web/server-side-exec.md | 0 .../tree/references/ctf/web/server-side.md | 0 .../tree/references/ctf/web/sql-injection.md | 0 .../seed/tree/references/ctf/web/web3.md | 0 .../seed/tree/references/ctf/writeup/index.md | 0 .../references/environment-and-resources.md | 0 .../seed/tree/references/evidence-workflow.md | 0 .../seed/tree/references/experience-index.md | 0 .../references/experience/deobfuscation.md | 0 .../tree/references/experience/pitfalls.md | 0 .../experience/reverse-unpacking.md | 0 .../tree/references/experience/web-captcha.md | 0 .../tree/references/field-journal-template.md | 0 .../registry/seed/tree/references/malware.md | 0 .../registry/seed/tree/references/pentest.md | 0 .../seed/tree/references/precedent-auth.md | 0 .../tree/references/progressive-reasoning.md | 0 .../seed/tree/references/reporting.md | 0 .../registry/seed/tree/references/reverse.md | 0 .../registry/seed/tree/references/routing.md | 0 .../registry/seed/tree/references/scanner.md | 0 .../tree/references/scope-and-evidence.md | 0 .../seed/tree/references/source-provenance.md | 0 .../registry/seed/tree/references/tools.md | 0 .../tree/schemas/research-result.schema.json | 0 .../registry/seed/tree/scripts/env_probe.py | 0 .../scripts/reusable/artifact_inventory.py | 0 .../seed/tree/scripts/reusable/har_summary.py | 0 .../scripts/reusable/new-experience-entry.ps1 | 0 .../scripts/reusable/new_experience_entry.py | 0 .../scripts/reusable/pack_cloud_handoff.py | 0 .../scripts/reusable/pe_entropy_triage.py | 0 .../seed/tree/scripts/reusable/route_task.py | 0 .../tree/scripts/reusable/scaffold_project.py | 0 .../seed/tree/scripts/validate_result.py | 0 .../seed/tree/scripts/validate_skill.py | 0 .../skills/sec-ai-security/INSTRUCTIONS.md | 0 .../llm-deep/_llm-security-workflow.md | 0 .../llm-deep/agent-obedience-engineering.md | 0 .../llm-deep/agent-security-testing.md | 0 .../references/llm-deep/owasp-llm-top10.md | 0 .../llm-deep/prompt-injection-methodology.md | 0 .../sec-assessment-tooling/INSTRUCTIONS.md | 0 .../pentest-tools/INSTRUCTIONS.md | 0 .../pentest-tools/payloads/README.md | 0 .../references/ai-pentest-agents.md | 0 .../ai-pentesting-landscape-2026.md | 0 .../references/automation-loop-pattern.md | 0 .../references/awesome-pentest-digest.md | 0 .../references/burpsuite-mcp-guide.md | 0 .../references/kali-mcp-ecosystem.md | 0 .../pentest-tools/references/msf-protocol.md | 0 .../references/network-attack-defense.md | 0 .../references/nuclei-guide-2026.md | 0 .../references/pentest-ai-agents-matrix.md | 0 .../pentest-tools/references/pentest-loop.md | 0 .../references/web-attack-cheatsheet.md | 0 .../.claude-plugin/marketplace.json | 0 .../pentest-tools/src-hunter/.gitignore | 0 .../pentest-tools/src-hunter/INSTRUCTIONS.md | 0 .../pentest-tools/src-hunter/LICENSE | 0 .../pentest-tools/src-hunter/README.en.md | 0 .../pentest-tools/src-hunter/README.md | 0 .../references/dictionaries/00-index.md | 0 .../dictionaries/chinese-srcfingerprints.md | 0 .../dictionaries/default-credentials-cn.md | 0 .../references/industry/00-index.md | 0 .../references/industry/banking-finance.md | 0 .../references/industry/telecom-isp.md | 0 .../references/methodology/00-index.md | 0 .../methodology/01-attack-priority.md | 0 .../methodology/02-bypass-toolkit.md | 0 .../methodology/03-evidence-discipline.md | 0 .../methodology/04-control-gap-hunting.md | 0 .../methodology/05-srctimebox-priority.md | 0 .../intranet/adcs\346\224\273\345\207\273.md" | 0 .../exchange\346\224\273\345\207\273.md" | 0 .../sharepoint\346\224\273\345\207\273.md" | 0 ...41\346\201\257\346\224\266\351\233\206.md" | 0 ...00\344\270\216\350\247\204\351\201\277.md" | 0 ...55\350\257\201\347\252\203\345\217\226.md" | 0 ...27\351\200\217\346\224\273\345\207\273.md" | 0 ...03\351\231\220\346\217\220\345\215\207.md" | 0 ...03\351\231\220\347\273\264\346\214\201.md" | 0 ...52\345\220\221\347\247\273\345\212\250.md" | 0 ...47\351\201\223\344\273\243\347\220\206.md" | 0 .../web/ai\345\256\211\345\205\250.md" | 0 .../web/api\345\256\211\345\205\250.md" | 0 ...67\346\261\202\344\274\252\351\200\240.md" | 0 .../web/jwt\345\256\211\345\205\250.md" | 0 ...07\344\273\266\345\214\205\345\220\253.md" | 0 ...43\347\240\201\346\211\247\350\241\214.md" | 0 .../web/sql-nosql\346\263\250\345\205\245.md" | 0 ...67\346\261\202\344\274\252\351\200\240.md" | 0 ...41\346\235\277\346\263\250\345\205\245.md" | 0 .../web/websocket\345\256\211\345\205\250.md" | 0 ...50\347\253\231\350\204\232\346\234\254.md" | 0 ...36\344\275\223\346\263\250\345\205\245.md" | 0 ...73\350\276\221\346\274\217\346\264\236.md" | 0 ...11\345\205\250\346\274\217\346\264\236.md" | 0 ...24\351\223\276\346\224\273\345\207\273.md" | 0 ...13\351\223\276\346\261\241\346\237\223.md" | 0 ...76\351\207\215\345\256\232\345\220\221.md" | 0 ...07\344\273\266\346\274\217\346\264\236.md" | Bin ...06\346\236\266\346\274\217\346\264\236.md" | 0 ...71\345\207\273\345\212\253\346\214\201.md" | 0 ...344\270\216cdn\345\256\211\345\205\250.md" | 0 ...44\350\257\201\346\274\217\346\264\236.md" | 0 ...67\346\261\202\350\265\260\347\247\201.md" | 0 .../src-hunter/references/payloader/index.md | 0 .../references/payloader/raw/navigation.json | 0 .../references/payloader/raw/tools.json | 0 .../references/payloader/raw/web.json | 0 .../tools/web\346\270\227\351\200\217.md" | 0 .../tools/windows\346\270\227\351\200\217.md" | 0 ...41\346\201\257\346\224\266\351\233\206.md" | 0 ...05\347\275\221\346\270\227\351\200\217.md" | 0 ...55\350\257\201\347\252\203\345\217\226.md" | 0 .../\345\237\237\346\270\227\351\200\217.md" | 0 ...06\347\240\201\346\224\273\345\207\273.md" | 0 ...03\351\231\220\346\217\220\345\215\207.md" | 0 ...17\346\264\236\345\210\251\347\224\250.md" | 0 ...73\347\273\237\345\221\275\344\273\244.md" | 0 ...42\351\230\237\345\267\245\345\205\267.md" | 0 ...26\347\240\201\350\247\243\347\240\201.md" | 0 ...47\351\201\223\344\273\243\347\220\206.md" | 0 .../references/playbooks/00-index.md | 0 .../references/playbooks/api-rest.md | 0 .../references/playbooks/arbitrary-x-authz.md | 0 .../src-hunter/references/playbooks/dos.md | 0 .../references/playbooks/file-upload.md | 0 .../references/playbooks/graphql.md | 0 .../references/playbooks/http-smuggling.md | 0 .../references/playbooks/info-disclosure.md | 0 .../references/playbooks/intranet-postexp.md | 0 .../playbooks/llm-prompt-injection.md | 0 .../references/playbooks/logic-flaws.md | 0 .../src-hunter/references/playbooks/mobile.md | 0 .../references/playbooks/oauth-saml-jwt.md | 0 .../references/playbooks/path-traversal.md | 0 .../references/playbooks/race-conditions.md | 0 .../src-hunter/references/playbooks/rce.md | 0 .../src-hunter/references/playbooks/sqli.md | 0 .../references/playbooks/ssrf-cache-host.md | 0 .../references/playbooks/unauth-access.md | 0 .../src-hunter/references/playbooks/xss.md | 0 .../references/templates/report-submission.md | 0 .../src-hunter/references/tools/mcp-jshook.md | 0 .../pentest-tools/templates/findings.md | 0 .../pentest-tools/templates/progress.md | 0 .../pentest-tools/templates/rules.md | 0 .../pentest-tools/templates/scope.md | 0 .../pentest-tools/templates/task_plan.md | 0 .../skills/sec-attack-chain/INSTRUCTIONS.md | 0 .../references/attack-playbooks.md | 0 .../references/evasion-cheatsheet.md | 0 .../tree/skills/sec-crypto/INSTRUCTIONS.md | 0 .../skills/sec-edr-bypass/INSTRUCTIONS.md | 0 .../sec-edr-bypass/references/hook-survey.md | 0 .../references/telemetry-blinding.md | 0 .../references/unhook-techniques.md | 0 .../tree/skills/sec-firmware/INSTRUCTIONS.md | 0 .../references/emba-automated-analysis.md | 0 .../references/emulation-and-fuzz.md | 0 .../references/extraction-methodology.md | 0 .../skills/sec-forensics-dfir/INSTRUCTIONS.md | 0 .../tree/skills/sec-malware/INSTRUCTIONS.md | 0 .../malware-deep/_malware-workflow.md | 0 .../malware-deep/anti-analysis-techniques.md | 0 .../malware-deep/sandbox-orchestration.md | 0 .../malware-deep/yara-sigma-rules.md | 0 .../seed/tree/skills/sec-misc/INSTRUCTIONS.md | 0 .../tree/skills/sec-osint/INSTRUCTIONS.md | 0 .../skills/sec-patch-diff/INSTRUCTIONS.md | 0 .../references/diff-tools-comparison.md | 0 .../references/patch-tuesday-workflow.md | 0 .../references/root-cause-and-poc.md | 0 .../tree/skills/sec-pwn-chain/INSTRUCTIONS.md | 0 .../sec-pwn-chain/references/heap-pwn.md | 0 .../sec-pwn-chain/references/kernel-pwn.md | 0 .../sec-pwn-chain/references/stack-pwn.md | 0 .../skills/sec-pwn-native/INSTRUCTIONS.md | 0 .../tree/skills/sec-reporting/INSTRUCTIONS.md | 0 .../tree/skills/sec-reverse/INSTRUCTIONS.md | 0 .../skills/sec-supply-chain/INSTRUCTIONS.md | 0 .../references/cicd-pipeline-security.md | 0 .../references/sbom-sca-methodology.md | 0 .../tree/skills/sec-web-api/INSTRUCTIONS.md | 0 .../api-deep/_api-security-workflow.md | 0 .../references/api-deep/jwt-oauth-testing.md | 0 .../api-deep/rest-graphql-testing.md | 0 .../promptskills}/registry/support.go | 0 .../promptskills}/registry/validation.go | 0 .../internal/promptskills}/source/gpt56.go | 0 .../promptskills}/source/gpt56_test.go | 0 .../source/prompts/gpt_5_6_instruct_v45.md | 0 .../internal/promptskills}/source/support.go | 0 .../business_system_prompt_repository_test.go | 2 +- backend/internal/server/routes/admin.go | 74 +- ...siness_system_prompt_account_scope_test.go | 107 +-- .../service/business_system_prompt_policy.go | 6 +- ...siness_system_prompt_rules_gateway_test.go | 2 +- .../business_system_prompt_rules_test.go | 2 +- .../business_system_prompt_rules_ws.go | 2 +- .../service/business_system_prompt_service.go | 8 +- .../service/openai_business_system_prompt.go | 2 +- .../service/plugin_domain_operation_test.go | 23 - .../service/plugin_extension_runtime_test.go | 81 -- .../service/plugin_prompt_fixture_test.go | 2 +- .../service/plugin_prompt_profile_test.go | 42 +- .../internal/service/plugin_prompt_test.go | 2 +- .../internal/service/prompt_domain_runtime.go | 6 +- .../service/prompt_domain_runtime_test.go | 89 --- ...mpt_management.go => prompt_management.go} | 14 +- .../internal/service/prompt_skills_module.go | 25 + .../internal/service/remote_skill_registry.go | 7 - .../remote_skill_registry_filesystem.go | 2 +- .../service/remote_skill_registry_manifest.go | 4 +- .../service/remote_skill_seed_fixture_test.go | 4 +- backend/internal/testextensions/fixtures.go | 2 +- .../src/api/admin/systemPromptRules.ts | 18 +- .../admin/account/AccountActionMenu.vue | 18 +- .../admin/account/AccountBulkActionsBar.vue | 21 +- .../account/AccountPromptBindingPanel.vue | 61 +- .../systemPrompt}/PromptRulesManager.vue | 9 +- .../SystemPromptAdvancedDrawer.vue | 5 +- .../__tests__/PromptRules.spec.ts | 18 +- frontend/src/components/layout/AppSidebar.vue | 2 +- frontend/src/i18n/locales/en/admin/index.ts | 2 + .../locales/en/admin/systemPromptRules.ts | 0 .../i18n/locales/en/admin/systemPrompts.ts | 3 +- frontend/src/i18n/locales/zh/admin/index.ts | 2 + .../locales/zh/admin/systemPromptRules.ts | 0 .../i18n/locales/zh/admin/systemPrompts.ts | 3 +- frontend/src/utils/accountPromptBinding.ts | 8 + .../src/views/admin/SystemPromptsView.vue | 687 ++++++++++++++++- .../SystemPromptsView.locale.spec.ts | 14 +- .../admin/__tests__/SystemPromptsView.spec.ts | 10 +- .../systemPromptsIntegrationSurface.spec.ts | 13 +- plugins/bundle.source.json | 1 - plugins/prompt-skills/cmd/plugin/main.go | 12 - plugins/prompt-skills/go.mod | 30 - plugins/prompt-skills/go.sum | 84 --- plugins/prompt-skills/manifest.source.json | 52 -- plugins/prompt-skills/scripts/check-ui.py | 130 ---- plugins/prompt-skills/ui/assets/app.js | 19 - plugins/prompt-skills/ui/assets/styles.css | 4 - plugins/prompt-skills/ui/index.html | 3 - .../ui/src/AccountPromptBinding.vue | 60 -- plugins/prompt-skills/ui/src/App.vue | 694 ------------------ plugins/prompt-skills/ui/src/api.ts | 301 -------- plugins/prompt-skills/ui/src/index.html | 2 - plugins/prompt-skills/ui/src/main.ts | 10 - 537 files changed, 988 insertions(+), 1854 deletions(-) rename {plugins/prompt-skills => backend/internal/promptskills}/policy/management.go (99%) rename {plugins/prompt-skills => backend/internal/promptskills}/policy/module.go (98%) rename {plugins/prompt-skills => backend/internal/promptskills}/policy/module_test.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/policy/rules.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/policy/rules_test.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/controller.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/manifest.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/prompts/codexrip_reverse_skill_system_prompt.txt (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/publication_test.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/references.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/manifest.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/pinned/waf-bypass.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/CHANGELOG.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/LICENSE (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/NOTICE.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/README_AI.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/RULES.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/SKILL.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/assets/templates/cloud-handoff-prompt.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/assets/templates/ctf-writeup.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/assets/templates/mission-state.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/assets/templates/research-result.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/assets/templates/writeups-index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/LICENSE (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/README.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/references/ad-certificate-abuse.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/references/agent-cloud.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-android-hooking/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-android-hooking/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-android-hooking/references/android-hooking.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/references/browser-persistence.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/references/bundle-sourcemap-recovery.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/references/cloud-metadata-path.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-container-runtime/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-container-runtime/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-container-runtime/references/container-runtime.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/references/crypto-mobile.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/references/custom-protocol-replay.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/references/dpapi-credential-chain.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/references/file-parser-chain.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/references/firmware-layout.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/references/forensic-timeline.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/references/graphql-rpc-drift.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-identity-windows/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-identity-windows/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-identity-windows/references/identity-windows.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/references/ios-runtime.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/references/jwt-claim-confusion.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/references/k8s-control-plane.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/references/kerberos-delegation.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/references/kernel-container-escape.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/references/linux-credential-pivot.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/references/lsass-ticket-material.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/references/mailbox-abuse.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-malware-config/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-malware-config/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-malware-config/references/malware-config.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/references/oauth-oidc-chain.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/references/pcap-protocol.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/references/prompt-injection.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/references/queue-worker-drift.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/references/race-condition-state-drift.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/references/relay-coercion-chain.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/references/request-normalization-smuggling.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/references/reverse-pwn.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/references/runtime-routing.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/references/ssrf-metadata-pivot.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-stego-media/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-stego-media/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-stego-media/references/stego-media.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-supply-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-supply-chain/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-supply-chain/references/supply-chain.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-template-render-path/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-template-render-path/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-template-render-path/references/template-render-path.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-web-runtime/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-web-runtime/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/cookie-hmac-key-reuse-auth-bypass.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/routing-runtime.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/references/websocket-runtime.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/references/windows-pivot.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/agents/openai.yaml (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/agent-cloud.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/crypto-mobile.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/ctf-resources-digest.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/identity-windows.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reporting.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reverse-native.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/router-matrix.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/web-api.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/evals.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/files/decoy-flags.txt (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/files/redaction-sample.har (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/files/runtime-conflict/capture-summary.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/files/runtime-conflict/source-note.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/files/safe_handler.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/files/untrusted-agent-document.txt (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/evals/trigger-evals.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ai-security.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/ai-ml/adversarial-ml.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/ai-ml/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/ai-ml/llm-attacks.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/ai-ml/model-attacks.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/advanced-math.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/classic-ciphers.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/ecc-attacks.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/exotic-crypto-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/exotic-crypto.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/historical.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/lattice-and-lwe.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/modern-ciphers-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/modern-ciphers-3.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/modern-ciphers.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/prng-attacks.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/prng.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/rsa-attacks-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/rsa-attacks.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/stream-ciphers.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/crypto/zkp-and-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/3d-printing.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/disk-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/disk-and-memory.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/disk-recovery.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/linux-forensics.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/network-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/network.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/peripheral-capture.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/signals-and-hardware.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/steganography.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/stego-advanced-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/stego-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/stego-image.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/forensics/windows.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/malware/c2-and-protocols.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/malware/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/malware/pe-and-dotnet.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/malware/scripts-and-obfuscation.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/bashjails.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/ctfd-navigation.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/dns.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/encodings-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/encodings.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/games-and-vms-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/games-and-vms-3.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/games-and-vms-4.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/games-and-vms.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/linux-privesc.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/pyjails.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/misc/rf-sdr.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/osint/geolocation-and-media.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/osint/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/osint/social-media.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/osint/web-and-dns.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/advanced-exploits-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/advanced-exploits-3.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/advanced-exploits-4.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/advanced-exploits-5.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/advanced-exploits.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/field-notes.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/format-string.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/heap-fsop.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/heap-techniques-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/heap-techniques.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/kernel-bypass.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/kernel-techniques.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/kernel.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/overflow-basics.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/rop-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/rop-and-shellcode.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/pwn/sandbox-escape.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/anti-analysis-ctf.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/anti-analysis.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/field-notes.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/languages-compiled.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/languages-platforms.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/languages.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/patterns-ctf-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/patterns-ctf-3.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/patterns-ctf.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/patterns-runtime.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/patterns.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/platforms-hardware.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/platforms.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/tools-advanced-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/tools-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/tools-dynamic.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/tools-emulation.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/reverse/tools.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/auth-and-access-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/auth-and-access.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/auth-infra.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/auth-jwt.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/client-side-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/client-side.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/cves.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/field-notes.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/node-and-prototype.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-advanced-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-advanced-3.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-advanced-4.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-advanced.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-deser.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-exec-2.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side-exec.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/server-side.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/sql-injection.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/web/web3.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/ctf/writeup/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/environment-and-resources.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/evidence-workflow.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/experience-index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/experience/deobfuscation.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/experience/pitfalls.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/experience/reverse-unpacking.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/experience/web-captcha.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/field-journal-template.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/malware.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/pentest.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/precedent-auth.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/progressive-reasoning.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/reporting.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/reverse.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/routing.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/scanner.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/scope-and-evidence.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/source-provenance.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/references/tools.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/schemas/research-result.schema.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/env_probe.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/artifact_inventory.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/har_summary.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/new-experience-entry.ps1 (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/new_experience_entry.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/pack_cloud_handoff.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/pe_entropy_triage.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/route_task.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/reusable/scaffold_project.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/validate_result.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/scripts/validate_skill.py (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-ai-security/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-ai-security/references/llm-deep/_llm-security-workflow.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-obedience-engineering.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-security-testing.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-ai-security/references/llm-deep/owasp-llm-top10.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-ai-security/references/llm-deep/prompt-injection-methodology.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/payloads/README.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentest-agents.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentesting-landscape-2026.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/automation-loop-pattern.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/awesome-pentest-digest.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/burpsuite-mcp-guide.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/kali-mcp-ecosystem.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/msf-protocol.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/network-attack-defense.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/nuclei-guide-2026.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-ai-agents-matrix.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-loop.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/web-attack-cheatsheet.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.claude-plugin/marketplace.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.gitignore (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/LICENSE (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.en.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/00-index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/chinese-srcfingerprints.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/default-credentials-cn.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/00-index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/banking-finance.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/telecom-isp.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/00-index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/01-attack-priority.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/02-bypass-toolkit.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/03-evidence-discipline.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/04-control-gap-hunting.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/05-srctimebox-priority.md (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/adcs\346\224\273\345\207\273.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/adcs\346\224\273\345\207\273.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/exchange\346\224\273\345\207\273.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/exchange\346\224\273\345\207\273.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/sharepoint\346\224\273\345\207\273.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/sharepoint\346\224\273\345\207\273.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\344\277\241\346\201\257\346\224\266\351\233\206.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\344\277\241\346\201\257\346\224\266\351\233\206.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\205\215\346\235\200\344\270\216\350\247\204\351\201\277.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\205\215\346\235\200\344\270\216\350\247\204\351\201\277.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\207\255\350\257\201\347\252\203\345\217\226.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\207\255\350\257\201\347\252\203\345\217\226.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\237\237\346\270\227\351\200\217\346\224\273\345\207\273.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\237\237\346\270\227\351\200\217\346\224\273\345\207\273.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\346\217\220\345\215\207.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\346\217\220\345\215\207.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\347\273\264\346\214\201.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\347\273\264\346\214\201.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\250\252\345\220\221\347\247\273\345\212\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\250\252\345\220\221\347\247\273\345\212\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\351\232\247\351\201\223\344\273\243\347\220\206.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\351\232\247\351\201\223\344\273\243\347\220\206.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ai\345\256\211\345\205\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ai\345\256\211\345\205\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/api\345\256\211\345\205\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/api\345\256\211\345\205\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/csrf\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/csrf\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/jwt\345\256\211\345\205\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/jwt\345\256\211\345\205\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/lfi-rfi\346\226\207\344\273\266\345\214\205\345\220\253.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/lfi-rfi\346\226\207\344\273\266\345\214\205\345\220\253.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/rce\350\277\234\347\250\213\344\273\243\347\240\201\346\211\247\350\241\214.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/rce\350\277\234\347\250\213\344\273\243\347\240\201\346\211\247\350\241\214.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/sql-nosql\346\263\250\345\205\245.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/sql-nosql\346\263\250\345\205\245.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssrf\346\234\215\345\212\241\347\253\257\350\257\267\346\261\202\344\274\252\351\200\240.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssrf\346\234\215\345\212\241\347\253\257\350\257\267\346\261\202\344\274\252\351\200\240.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssti\346\250\241\346\235\277\346\263\250\345\205\245.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssti\346\250\241\346\235\277\346\263\250\345\205\245.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/websocket\345\256\211\345\205\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/websocket\345\256\211\345\205\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xss\350\267\250\347\253\231\350\204\232\346\234\254.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xss\350\267\250\347\253\231\350\204\232\346\234\254.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xxe\345\256\236\344\275\223\346\263\250\345\205\245.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xxe\345\256\236\344\275\223\346\263\250\345\205\245.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\270\232\345\212\241\351\200\273\350\276\221\346\274\217\346\264\236.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\270\232\345\212\241\351\200\273\350\276\221\346\274\217\346\264\236.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\272\221\345\256\211\345\205\250\346\274\217\346\264\236.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\272\221\345\256\211\345\205\250\346\274\217\346\264\236.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\276\233\345\272\224\351\223\276\346\224\273\345\207\273.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\276\233\345\272\224\351\223\276\346\224\273\345\207\273.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\216\237\345\236\213\351\223\276\346\261\241\346\237\223.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\216\237\345\236\213\351\223\276\346\261\241\346\237\223.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\274\200\346\224\276\351\207\215\345\256\232\345\220\221.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\274\200\346\224\276\351\207\215\345\256\232\345\220\221.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\226\207\344\273\266\346\274\217\346\264\236.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\226\207\344\273\266\346\274\217\346\264\236.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\241\206\346\236\266\346\274\217\346\264\236.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\241\206\346\236\266\346\274\217\346\264\236.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\202\271\345\207\273\345\212\253\346\214\201.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\202\271\345\207\273\345\212\253\346\214\201.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\274\223\345\255\230\344\270\216cdn\345\256\211\345\205\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\274\223\345\255\230\344\270\216cdn\345\256\211\345\205\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\256\244\350\257\201\346\274\217\346\264\236.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\256\244\350\257\201\346\274\217\346\264\236.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\257\267\346\261\202\350\265\260\347\247\201.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\257\267\346\261\202\350\265\260\347\247\201.md" (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/navigation.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/tools.json (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/web.json (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/web\346\270\227\351\200\217.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/web\346\270\227\351\200\217.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/windows\346\270\227\351\200\217.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/windows\346\270\227\351\200\217.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\344\277\241\346\201\257\346\224\266\351\233\206.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\344\277\241\346\201\257\346\224\266\351\233\206.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\206\205\347\275\221\346\270\227\351\200\217.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\206\205\347\275\221\346\270\227\351\200\217.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\207\255\350\257\201\347\252\203\345\217\226.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\207\255\350\257\201\347\252\203\345\217\226.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\237\237\346\270\227\351\200\217.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\237\237\346\270\227\351\200\217.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\257\206\347\240\201\346\224\273\345\207\273.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\257\206\347\240\201\346\224\273\345\207\273.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\235\203\351\231\220\346\217\220\345\215\207.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\235\203\351\231\220\346\217\220\345\215\207.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\274\217\346\264\236\345\210\251\347\224\250.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\274\217\346\264\236\345\210\251\347\224\250.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\263\273\347\273\237\345\221\275\344\273\244.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\263\273\347\273\237\345\221\275\344\273\244.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\272\242\351\230\237\345\267\245\345\205\267.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\272\242\351\230\237\345\267\245\345\205\267.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\274\226\347\240\201\350\247\243\347\240\201.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\274\226\347\240\201\350\247\243\347\240\201.md" (100%) rename "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\351\232\247\351\201\223\344\273\243\347\220\206.md" => "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\351\232\247\351\201\223\344\273\243\347\220\206.md" (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/00-index.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/api-rest.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/arbitrary-x-authz.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/dos.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/file-upload.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/graphql.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/http-smuggling.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/info-disclosure.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/intranet-postexp.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/llm-prompt-injection.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/logic-flaws.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/mobile.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/oauth-saml-jwt.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/path-traversal.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/race-conditions.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/rce.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/sqli.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/ssrf-cache-host.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/unauth-access.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/xss.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/templates/report-submission.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/tools/mcp-jshook.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/findings.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/progress.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/rules.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/scope.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/task_plan.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-attack-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-attack-chain/references/attack-playbooks.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-attack-chain/references/evasion-cheatsheet.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-crypto/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-edr-bypass/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-edr-bypass/references/hook-survey.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-edr-bypass/references/telemetry-blinding.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-edr-bypass/references/unhook-techniques.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-firmware/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-firmware/references/emba-automated-analysis.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-firmware/references/emulation-and-fuzz.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-firmware/references/extraction-methodology.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-forensics-dfir/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-malware/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-malware/references/malware-deep/_malware-workflow.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-malware/references/malware-deep/anti-analysis-techniques.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-malware/references/malware-deep/sandbox-orchestration.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-malware/references/malware-deep/yara-sigma-rules.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-misc/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-osint/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-patch-diff/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-patch-diff/references/diff-tools-comparison.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-patch-diff/references/patch-tuesday-workflow.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-patch-diff/references/root-cause-and-poc.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-pwn-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-pwn-chain/references/heap-pwn.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-pwn-chain/references/kernel-pwn.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-pwn-chain/references/stack-pwn.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-pwn-native/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-reporting/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-reverse/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-supply-chain/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-supply-chain/references/cicd-pipeline-security.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-supply-chain/references/sbom-sca-methodology.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-web-api/INSTRUCTIONS.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-web-api/references/api-deep/_api-security-workflow.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-web-api/references/api-deep/jwt-oauth-testing.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/seed/tree/skills/sec-web-api/references/api-deep/rest-graphql-testing.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/support.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/registry/validation.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/source/gpt56.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/source/gpt56_test.go (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/source/prompts/gpt_5_6_instruct_v45.md (100%) rename {plugins/prompt-skills => backend/internal/promptskills}/source/support.go (100%) rename backend/internal/service/{plugin_prompt_management.go => prompt_management.go} (89%) create mode 100644 backend/internal/service/prompt_skills_module.go rename plugins/prompt-skills/ui/src/rules-api.ts => frontend/src/api/admin/systemPromptRules.ts (64%) rename {plugins/prompt-skills/ui/src => frontend/src/components/admin/systemPrompt}/PromptRulesManager.vue (98%) rename {plugins/prompt-skills/ui/src => frontend/src/components/admin/systemPrompt}/SystemPromptAdvancedDrawer.vue (99%) rename {plugins/prompt-skills/ui/src => frontend/src/components/admin/systemPrompt}/__tests__/PromptRules.spec.ts (85%) rename plugins/prompt-skills/ui/src/locales/rules.en.ts => frontend/src/i18n/locales/en/admin/systemPromptRules.ts (100%) rename plugins/prompt-skills/ui/src/locales/en.ts => frontend/src/i18n/locales/en/admin/systemPrompts.ts (98%) rename plugins/prompt-skills/ui/src/locales/rules.zh.ts => frontend/src/i18n/locales/zh/admin/systemPromptRules.ts (100%) rename plugins/prompt-skills/ui/src/locales/zh.ts => frontend/src/i18n/locales/zh/admin/systemPrompts.ts (98%) create mode 100644 frontend/src/utils/accountPromptBinding.ts rename plugins/prompt-skills/ui/src/__tests__/locales.spec.ts => frontend/src/views/admin/__tests__/SystemPromptsView.locale.spec.ts (93%) rename plugins/prompt-skills/ui/src/__tests__/App.spec.ts => frontend/src/views/admin/__tests__/SystemPromptsView.spec.ts (98%) delete mode 100644 plugins/prompt-skills/cmd/plugin/main.go delete mode 100644 plugins/prompt-skills/go.mod delete mode 100644 plugins/prompt-skills/go.sum delete mode 100644 plugins/prompt-skills/manifest.source.json delete mode 100644 plugins/prompt-skills/scripts/check-ui.py delete mode 100644 plugins/prompt-skills/ui/assets/app.js delete mode 100644 plugins/prompt-skills/ui/assets/styles.css delete mode 100644 plugins/prompt-skills/ui/index.html delete mode 100644 plugins/prompt-skills/ui/src/AccountPromptBinding.vue delete mode 100644 plugins/prompt-skills/ui/src/App.vue delete mode 100644 plugins/prompt-skills/ui/src/api.ts delete mode 100644 plugins/prompt-skills/ui/src/index.html delete mode 100644 plugins/prompt-skills/ui/src/main.ts diff --git a/.gitattributes b/.gitattributes index c31b8b16c4e1..125d260cd1c0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,15 +6,15 @@ backend/migrations/*.sql text eol=lf # Embedded system prompt seed must retain the exact LF byte sequence used for # its published SHA-256 and UTF-8 byte-length invariants. -plugins/prompt-skills/registry/prompts/*.txt text eol=lf -plugins/prompt-skills/source/prompts/*.md text eol=lf +backend/internal/promptskills/registry/prompts/*.txt text eol=lf +backend/internal/promptskills/source/prompts/*.md text eol=lf backend/internal/service/testdata/codexrip_reverse_skill_system_prompt_legacy_dual_marker.txt text eol=lf # The approved ModelGang seed is UTF-8 text, including extensionless and # Unicode-named files. Keep every embedded byte sequence stable cross-platform. -plugins/prompt-skills/registry/seed/manifest.json text eol=lf -whitespace -plugins/prompt-skills/registry/seed/tree/** text eol=lf -whitespace -plugins/prompt-skills/registry/seed/pinned/** text eol=lf -whitespace +backend/internal/promptskills/registry/seed/manifest.json text eol=lf -whitespace +backend/internal/promptskills/registry/seed/tree/** text eol=lf -whitespace +backend/internal/promptskills/registry/seed/pinned/** text eol=lf -whitespace # 前端 源代码文件 *.ts text eol=lf diff --git a/.github/scripts/test_first_party_plugins.sh b/.github/scripts/test_first_party_plugins.sh index 33735d1d60d9..bddfa7b05502 100644 --- a/.github/scripts/test_first_party_plugins.sh +++ b/.github/scripts/test_first_party_plugins.sh @@ -29,9 +29,8 @@ go build -trimpath -o "$plugin_build_dir/package-plugin" ./cmd/package-plugin export SUB2API_EXTENSION_TEST_BINARY="$plugin_build_dir/binaries/codex-runtime" export SUB2API_EXTENSION_TEST_BUNDLE="$plugin_build_dir/bundle/lock.json" -export SUB2API_PROMPT_SKILLS_TEST_BINARY="$plugin_build_dir/binaries/prompt-skills" export SUB2API_ACCOUNT_TOOLS_TEST_BINARY="$plugin_build_dir/binaries/account-tools" export SUB2API_CINDY_PROVIDER_TEST_BINARY="$plugin_build_dir/binaries/cindy-provider" export SUB2API_EXTENSION_TEST_PUBLIC_KEY SUB2API_EXTENSION_TEST_PUBLIC_KEY=$(jq -r .public_key "$plugin_build_dir/publisher.json") -go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestPromptExtensionRuntimeUsesIndependentProcess|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 +go test ./internal/service -run '^(TestExtensionRuntimeUsesOfficialProcessAndHostBroker|TestAccountToolsExtensionRuntimeUsesIndependentProcess|TestCindyProviderExtensionRuntimeUsesIndependentProcess|TestFirstPartyBundleContainsMatchingSignedDomainPackages)$' -count=1 diff --git a/.github/workflows/downstream-release.yml b/.github/workflows/downstream-release.yml index b7ff720fe890..2cb8038f3987 100644 --- a/.github/workflows/downstream-release.yml +++ b/.github/workflows/downstream-release.yml @@ -145,7 +145,7 @@ jobs: shell: bash run: | set -euo pipefail - actual="$(sha256sum plugins/prompt-skills/registry/prompts/codexrip_reverse_skill_system_prompt.txt | awk '{print $1}')" + actual="$(sha256sum backend/internal/promptskills/registry/prompts/codexrip_reverse_skill_system_prompt.txt | awk '{print $1}')" [[ "$actual" == "$PROMPT_SHA" ]] - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build release image without publication diff --git a/.github/workflows/downstream-verify.yml b/.github/workflows/downstream-verify.yml index c0ac4118637e..830c23d6cb3d 100644 --- a/.github/workflows/downstream-verify.yml +++ b/.github/workflows/downstream-verify.yml @@ -95,7 +95,7 @@ jobs: go test -p=1 ./internal/repository -run '^$' - name: Account view, create, and edit scope contracts working-directory: backend - run: go test -p=1 -tags unit ./internal/service ./internal/handler/admin ./internal/handler/dto ./internal/repository -run '^(TestAccountView.*|TestAccountCreate.*|TestAccountEdit.*|TestPluginManifest(SchemaDeclaresContributionFields|AcceptsRegisteredHyphenatedResources|ResourcePatternParityAndBoundaries)|TestCindyRecoveryResourceProjection)$' -count=1 + run: go test -p=1 -tags unit ./internal/service ./internal/handler/admin ./internal/handler/dto ./internal/repository -run '^(TestAccountView.*|TestAccountCreate.*|TestAccountEdit.*|TestPluginManifest(SchemaDeclaresContributionFields|ResourcePatternParityAndBoundaries)|TestCindyRecoveryResourceProjection)$' -count=1 - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: '3.12' @@ -104,9 +104,9 @@ jobs: python -m pip install 'PyYAML==6.0.2' candidate=$(mktemp -d)/security-research mkdir -p "$candidate" - cp -a plugins/prompt-skills/registry/seed/tree/. "$candidate/" + cp -a backend/internal/promptskills/registry/seed/tree/. "$candidate/" install -D -m 0644 \ - plugins/prompt-skills/registry/seed/pinned/waf-bypass.md \ + backend/internal/promptskills/registry/seed/pinned/waf-bypass.md \ "$candidate/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/waf-bypass.md" python "$candidate/scripts/validate_skill.py" "$candidate" --require-evals --json - name: Account taxonomy migration integration test @@ -209,9 +209,9 @@ jobs: src/api/__tests__/admin.accounts.contextCapacity.spec.ts \ src/composables/__tests__/useModelWhitelist.spec.ts \ src/components/account/__tests__/BulkEditAccountModal.spec.ts \ - ../plugins/prompt-skills/ui/src/__tests__/App.spec.ts \ - ../plugins/prompt-skills/ui/src/__tests__/PromptRules.spec.ts \ - ../plugins/prompt-skills/ui/src/__tests__/locales.spec.ts \ + src/views/admin/__tests__/SystemPromptsView.spec.ts \ + src/views/admin/__tests__/SystemPromptsView.locale.spec.ts \ + src/components/admin/systemPrompt/__tests__/PromptRules.spec.ts \ src/views/admin/__tests__/AccountsView.console.spec.ts \ src/views/admin/__tests__/AccountsView.priorityColumn.spec.ts \ src/features/image-studio/__tests__/api.spec.ts \ diff --git a/.gitignore b/.gitignore index d5b81c5de002..09b7c32f7581 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,8 @@ scripts !.github/scripts/*.py !backend/scripts/ !backend/scripts/resolve-version.sh +!/backend/internal/promptskills/registry/seed/tree/scripts/ +!/backend/internal/promptskills/registry/seed/tree/scripts/** .code-review-state #openspec/ code-reviews/ diff --git a/backend/go.mod b/backend/go.mod index 6d77f1c14759..95256d2e445e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,15 +4,12 @@ go 1.27.0 replace github.com/HTExplicit/sub2api-plugins/codexruntime => ../plugins/codex-runtime -replace github.com/HTExplicit/sub2api-plugins/promptskills => ../plugins/prompt-skills - replace github.com/HTExplicit/sub2api-plugins/accounttools => ../plugins/account-tools replace github.com/HTExplicit/sub2api-plugins/cindyprovider => ../plugins/cindy-provider require ( github.com/HTExplicit/sub2api-plugins/codexruntime v0.0.0 - github.com/HTExplicit/sub2api-plugins/promptskills v0.0.0 github.com/HTExplicit/sub2api-plugins/accounttools v0.0.0 github.com/HTExplicit/sub2api-plugins/cindyprovider v0.0.0 entgo.io/ent v0.14.5 diff --git a/plugins/prompt-skills/policy/management.go b/backend/internal/promptskills/policy/management.go similarity index 99% rename from plugins/prompt-skills/policy/management.go rename to backend/internal/promptskills/policy/management.go index 353529c87423..96a980441bcb 100644 --- a/plugins/prompt-skills/policy/management.go +++ b/backend/internal/promptskills/policy/management.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - "github.com/HTExplicit/sub2api-plugins/promptskills/source" + "github.com/Wei-Shaw/sub2api/internal/promptskills/source" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/pmezard/go-difflib/difflib" diff --git a/plugins/prompt-skills/policy/module.go b/backend/internal/promptskills/policy/module.go similarity index 98% rename from plugins/prompt-skills/policy/module.go rename to backend/internal/promptskills/policy/module.go index a06410d87037..1eefc1e54742 100644 --- a/plugins/prompt-skills/policy/module.go +++ b/backend/internal/promptskills/policy/module.go @@ -7,8 +7,8 @@ import ( "fmt" "strings" - "github.com/HTExplicit/sub2api-plugins/promptskills/registry" - "github.com/HTExplicit/sub2api-plugins/promptskills/source" + "github.com/Wei-Shaw/sub2api/internal/promptskills/registry" + "github.com/Wei-Shaw/sub2api/internal/promptskills/source" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) diff --git a/plugins/prompt-skills/policy/module_test.go b/backend/internal/promptskills/policy/module_test.go similarity index 100% rename from plugins/prompt-skills/policy/module_test.go rename to backend/internal/promptskills/policy/module_test.go diff --git a/plugins/prompt-skills/policy/rules.go b/backend/internal/promptskills/policy/rules.go similarity index 100% rename from plugins/prompt-skills/policy/rules.go rename to backend/internal/promptskills/policy/rules.go diff --git a/plugins/prompt-skills/policy/rules_test.go b/backend/internal/promptskills/policy/rules_test.go similarity index 100% rename from plugins/prompt-skills/policy/rules_test.go rename to backend/internal/promptskills/policy/rules_test.go diff --git a/plugins/prompt-skills/registry/controller.go b/backend/internal/promptskills/registry/controller.go similarity index 100% rename from plugins/prompt-skills/registry/controller.go rename to backend/internal/promptskills/registry/controller.go diff --git a/plugins/prompt-skills/registry/manifest.go b/backend/internal/promptskills/registry/manifest.go similarity index 100% rename from plugins/prompt-skills/registry/manifest.go rename to backend/internal/promptskills/registry/manifest.go diff --git a/plugins/prompt-skills/registry/prompts/codexrip_reverse_skill_system_prompt.txt b/backend/internal/promptskills/registry/prompts/codexrip_reverse_skill_system_prompt.txt similarity index 100% rename from plugins/prompt-skills/registry/prompts/codexrip_reverse_skill_system_prompt.txt rename to backend/internal/promptskills/registry/prompts/codexrip_reverse_skill_system_prompt.txt diff --git a/plugins/prompt-skills/registry/publication_test.go b/backend/internal/promptskills/registry/publication_test.go similarity index 100% rename from plugins/prompt-skills/registry/publication_test.go rename to backend/internal/promptskills/registry/publication_test.go diff --git a/plugins/prompt-skills/registry/references.go b/backend/internal/promptskills/registry/references.go similarity index 100% rename from plugins/prompt-skills/registry/references.go rename to backend/internal/promptskills/registry/references.go diff --git a/plugins/prompt-skills/registry/seed/manifest.json b/backend/internal/promptskills/registry/seed/manifest.json similarity index 100% rename from plugins/prompt-skills/registry/seed/manifest.json rename to backend/internal/promptskills/registry/seed/manifest.json diff --git a/plugins/prompt-skills/registry/seed/pinned/waf-bypass.md b/backend/internal/promptskills/registry/seed/pinned/waf-bypass.md similarity index 100% rename from plugins/prompt-skills/registry/seed/pinned/waf-bypass.md rename to backend/internal/promptskills/registry/seed/pinned/waf-bypass.md diff --git a/plugins/prompt-skills/registry/seed/tree/CHANGELOG.md b/backend/internal/promptskills/registry/seed/tree/CHANGELOG.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/CHANGELOG.md rename to backend/internal/promptskills/registry/seed/tree/CHANGELOG.md diff --git a/plugins/prompt-skills/registry/seed/tree/LICENSE b/backend/internal/promptskills/registry/seed/tree/LICENSE similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/LICENSE rename to backend/internal/promptskills/registry/seed/tree/LICENSE diff --git a/plugins/prompt-skills/registry/seed/tree/NOTICE.md b/backend/internal/promptskills/registry/seed/tree/NOTICE.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/NOTICE.md rename to backend/internal/promptskills/registry/seed/tree/NOTICE.md diff --git a/plugins/prompt-skills/registry/seed/tree/README_AI.md b/backend/internal/promptskills/registry/seed/tree/README_AI.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/README_AI.md rename to backend/internal/promptskills/registry/seed/tree/README_AI.md diff --git a/plugins/prompt-skills/registry/seed/tree/RULES.md b/backend/internal/promptskills/registry/seed/tree/RULES.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/RULES.md rename to backend/internal/promptskills/registry/seed/tree/RULES.md diff --git a/plugins/prompt-skills/registry/seed/tree/SKILL.md b/backend/internal/promptskills/registry/seed/tree/SKILL.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/SKILL.md rename to backend/internal/promptskills/registry/seed/tree/SKILL.md diff --git a/plugins/prompt-skills/registry/seed/tree/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/assets/templates/cloud-handoff-prompt.md b/backend/internal/promptskills/registry/seed/tree/assets/templates/cloud-handoff-prompt.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/assets/templates/cloud-handoff-prompt.md rename to backend/internal/promptskills/registry/seed/tree/assets/templates/cloud-handoff-prompt.md diff --git a/plugins/prompt-skills/registry/seed/tree/assets/templates/ctf-writeup.md b/backend/internal/promptskills/registry/seed/tree/assets/templates/ctf-writeup.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/assets/templates/ctf-writeup.md rename to backend/internal/promptskills/registry/seed/tree/assets/templates/ctf-writeup.md diff --git a/plugins/prompt-skills/registry/seed/tree/assets/templates/mission-state.md b/backend/internal/promptskills/registry/seed/tree/assets/templates/mission-state.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/assets/templates/mission-state.md rename to backend/internal/promptskills/registry/seed/tree/assets/templates/mission-state.md diff --git a/plugins/prompt-skills/registry/seed/tree/assets/templates/research-result.json b/backend/internal/promptskills/registry/seed/tree/assets/templates/research-result.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/assets/templates/research-result.json rename to backend/internal/promptskills/registry/seed/tree/assets/templates/research-result.json diff --git a/plugins/prompt-skills/registry/seed/tree/assets/templates/writeups-index.md b/backend/internal/promptskills/registry/seed/tree/assets/templates/writeups-index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/assets/templates/writeups-index.md rename to backend/internal/promptskills/registry/seed/tree/assets/templates/writeups-index.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/LICENSE b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/LICENSE similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/LICENSE rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/LICENSE diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/README.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/README.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/README.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/README.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/references/ad-certificate-abuse.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/references/ad-certificate-abuse.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/references/ad-certificate-abuse.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ad-certificate-abuse/references/ad-certificate-abuse.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/references/agent-cloud.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/references/agent-cloud.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/references/agent-cloud.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-agent-cloud/references/agent-cloud.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/references/android-hooking.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/references/android-hooking.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/references/android-hooking.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-android-hooking/references/android-hooking.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/references/browser-persistence.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/references/browser-persistence.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/references/browser-persistence.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-browser-persistence/references/browser-persistence.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/references/bundle-sourcemap-recovery.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/references/bundle-sourcemap-recovery.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/references/bundle-sourcemap-recovery.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-bundle-sourcemap-recovery/references/bundle-sourcemap-recovery.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/references/cloud-metadata-path.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/references/cloud-metadata-path.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/references/cloud-metadata-path.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-cloud-metadata-path/references/cloud-metadata-path.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/references/container-runtime.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/references/container-runtime.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/references/container-runtime.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-container-runtime/references/container-runtime.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/references/crypto-mobile.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/references/crypto-mobile.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/references/crypto-mobile.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-crypto-mobile/references/crypto-mobile.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/references/custom-protocol-replay.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/references/custom-protocol-replay.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/references/custom-protocol-replay.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-custom-protocol-replay/references/custom-protocol-replay.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/references/dpapi-credential-chain.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/references/dpapi-credential-chain.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/references/dpapi-credential-chain.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-dpapi-credential-chain/references/dpapi-credential-chain.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/references/file-parser-chain.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/references/file-parser-chain.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/references/file-parser-chain.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-file-parser-chain/references/file-parser-chain.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/references/firmware-layout.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/references/firmware-layout.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/references/firmware-layout.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-firmware-layout/references/firmware-layout.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/references/forensic-timeline.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/references/forensic-timeline.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/references/forensic-timeline.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-forensic-timeline/references/forensic-timeline.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/references/graphql-rpc-drift.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/references/graphql-rpc-drift.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/references/graphql-rpc-drift.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-graphql-rpc-drift/references/graphql-rpc-drift.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/references/identity-windows.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/references/identity-windows.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/references/identity-windows.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-identity-windows/references/identity-windows.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/references/ios-runtime.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/references/ios-runtime.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/references/ios-runtime.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ios-runtime/references/ios-runtime.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/references/jwt-claim-confusion.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/references/jwt-claim-confusion.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/references/jwt-claim-confusion.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-jwt-claim-confusion/references/jwt-claim-confusion.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/references/k8s-control-plane.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/references/k8s-control-plane.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/references/k8s-control-plane.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-k8s-control-plane/references/k8s-control-plane.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/references/kerberos-delegation.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/references/kerberos-delegation.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/references/kerberos-delegation.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kerberos-delegation/references/kerberos-delegation.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/references/kernel-container-escape.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/references/kernel-container-escape.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/references/kernel-container-escape.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-kernel-container-escape/references/kernel-container-escape.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/references/linux-credential-pivot.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/references/linux-credential-pivot.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/references/linux-credential-pivot.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-linux-credential-pivot/references/linux-credential-pivot.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/references/lsass-ticket-material.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/references/lsass-ticket-material.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/references/lsass-ticket-material.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-lsass-ticket-material/references/lsass-ticket-material.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/references/mailbox-abuse.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/references/mailbox-abuse.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/references/mailbox-abuse.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-mailbox-abuse/references/mailbox-abuse.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-malware-config/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-malware-config/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-malware-config/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-malware-config/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-malware-config/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-malware-config/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-malware-config/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-malware-config/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-malware-config/references/malware-config.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-malware-config/references/malware-config.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-malware-config/references/malware-config.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-malware-config/references/malware-config.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/references/oauth-oidc-chain.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/references/oauth-oidc-chain.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/references/oauth-oidc-chain.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-oauth-oidc-chain/references/oauth-oidc-chain.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/references/pcap-protocol.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/references/pcap-protocol.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/references/pcap-protocol.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-pcap-protocol/references/pcap-protocol.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/references/prompt-injection.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/references/prompt-injection.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/references/prompt-injection.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-prompt-injection/references/prompt-injection.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/references/queue-worker-drift.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/references/queue-worker-drift.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/references/queue-worker-drift.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-queue-worker-drift/references/queue-worker-drift.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/references/race-condition-state-drift.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/references/race-condition-state-drift.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/references/race-condition-state-drift.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-race-condition-state-drift/references/race-condition-state-drift.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/references/relay-coercion-chain.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/references/relay-coercion-chain.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/references/relay-coercion-chain.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-relay-coercion-chain/references/relay-coercion-chain.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/references/request-normalization-smuggling.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/references/request-normalization-smuggling.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/references/request-normalization-smuggling.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-request-normalization-smuggling/references/request-normalization-smuggling.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/references/reverse-pwn.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/references/reverse-pwn.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/references/reverse-pwn.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-reverse-pwn/references/reverse-pwn.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/references/runtime-routing.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/references/runtime-routing.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/references/runtime-routing.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-runtime-routing/references/runtime-routing.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/references/ssrf-metadata-pivot.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/references/ssrf-metadata-pivot.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/references/ssrf-metadata-pivot.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-ssrf-metadata-pivot/references/ssrf-metadata-pivot.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-stego-media/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-stego-media/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-stego-media/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-stego-media/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-stego-media/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-stego-media/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-stego-media/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-stego-media/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-stego-media/references/stego-media.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-stego-media/references/stego-media.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-stego-media/references/stego-media.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-stego-media/references/stego-media.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/references/supply-chain.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/references/supply-chain.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/references/supply-chain.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-supply-chain/references/supply-chain.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/references/template-render-path.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/references/template-render-path.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/references/template-render-path.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-template-render-path/references/template-render-path.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/cookie-hmac-key-reuse-auth-bypass.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/cookie-hmac-key-reuse-auth-bypass.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/cookie-hmac-key-reuse-auth-bypass.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/cookie-hmac-key-reuse-auth-bypass.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/routing-runtime.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/routing-runtime.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/routing-runtime.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-web-runtime/references/routing-runtime.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/references/websocket-runtime.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/references/websocket-runtime.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/references/websocket-runtime.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-websocket-runtime/references/websocket-runtime.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/references/windows-pivot.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/references/windows-pivot.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/references/windows-pivot.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/competition-windows-pivot/references/windows-pivot.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/agents/openai.yaml b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/agents/openai.yaml similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/agents/openai.yaml rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/agents/openai.yaml diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/agent-cloud.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/agent-cloud.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/agent-cloud.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/agent-cloud.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/crypto-mobile.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/crypto-mobile.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/crypto-mobile.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/crypto-mobile.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/ctf-resources-digest.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/ctf-resources-digest.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/ctf-resources-digest.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/ctf-resources-digest.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/identity-windows.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/identity-windows.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/identity-windows.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/identity-windows.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reporting.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reporting.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reporting.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reporting.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reverse-native.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reverse-native.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reverse-native.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/reverse-native.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/router-matrix.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/router-matrix.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/router-matrix.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/router-matrix.md diff --git a/plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/web-api.md b/backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/web-api.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/web-api.md rename to backend/internal/promptskills/registry/seed/tree/ctf-orchestrator/ctf-sandbox-orchestrator/references/web-api.md diff --git a/plugins/prompt-skills/registry/seed/tree/evals/evals.json b/backend/internal/promptskills/registry/seed/tree/evals/evals.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/evals.json rename to backend/internal/promptskills/registry/seed/tree/evals/evals.json diff --git a/plugins/prompt-skills/registry/seed/tree/evals/files/decoy-flags.txt b/backend/internal/promptskills/registry/seed/tree/evals/files/decoy-flags.txt similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/files/decoy-flags.txt rename to backend/internal/promptskills/registry/seed/tree/evals/files/decoy-flags.txt diff --git a/plugins/prompt-skills/registry/seed/tree/evals/files/redaction-sample.har b/backend/internal/promptskills/registry/seed/tree/evals/files/redaction-sample.har similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/files/redaction-sample.har rename to backend/internal/promptskills/registry/seed/tree/evals/files/redaction-sample.har diff --git a/plugins/prompt-skills/registry/seed/tree/evals/files/runtime-conflict/capture-summary.json b/backend/internal/promptskills/registry/seed/tree/evals/files/runtime-conflict/capture-summary.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/files/runtime-conflict/capture-summary.json rename to backend/internal/promptskills/registry/seed/tree/evals/files/runtime-conflict/capture-summary.json diff --git a/plugins/prompt-skills/registry/seed/tree/evals/files/runtime-conflict/source-note.md b/backend/internal/promptskills/registry/seed/tree/evals/files/runtime-conflict/source-note.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/files/runtime-conflict/source-note.md rename to backend/internal/promptskills/registry/seed/tree/evals/files/runtime-conflict/source-note.md diff --git a/plugins/prompt-skills/registry/seed/tree/evals/files/safe_handler.py b/backend/internal/promptskills/registry/seed/tree/evals/files/safe_handler.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/files/safe_handler.py rename to backend/internal/promptskills/registry/seed/tree/evals/files/safe_handler.py diff --git a/plugins/prompt-skills/registry/seed/tree/evals/files/untrusted-agent-document.txt b/backend/internal/promptskills/registry/seed/tree/evals/files/untrusted-agent-document.txt similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/files/untrusted-agent-document.txt rename to backend/internal/promptskills/registry/seed/tree/evals/files/untrusted-agent-document.txt diff --git a/plugins/prompt-skills/registry/seed/tree/evals/trigger-evals.json b/backend/internal/promptskills/registry/seed/tree/evals/trigger-evals.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/evals/trigger-evals.json rename to backend/internal/promptskills/registry/seed/tree/evals/trigger-evals.json diff --git a/plugins/prompt-skills/registry/seed/tree/references/ai-security.md b/backend/internal/promptskills/registry/seed/tree/references/ai-security.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ai-security.md rename to backend/internal/promptskills/registry/seed/tree/references/ai-security.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/adversarial-ml.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/adversarial-ml.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/adversarial-ml.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/adversarial-ml.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/llm-attacks.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/llm-attacks.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/llm-attacks.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/llm-attacks.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/model-attacks.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/model-attacks.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/ai-ml/model-attacks.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/ai-ml/model-attacks.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/advanced-math.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/advanced-math.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/advanced-math.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/advanced-math.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/classic-ciphers.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/classic-ciphers.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/classic-ciphers.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/classic-ciphers.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/ecc-attacks.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/ecc-attacks.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/ecc-attacks.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/ecc-attacks.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/exotic-crypto-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/exotic-crypto-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/exotic-crypto-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/exotic-crypto-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/exotic-crypto.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/exotic-crypto.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/exotic-crypto.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/exotic-crypto.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/historical.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/historical.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/historical.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/historical.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/lattice-and-lwe.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/lattice-and-lwe.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/lattice-and-lwe.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/lattice-and-lwe.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/modern-ciphers-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/modern-ciphers-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/modern-ciphers-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/modern-ciphers-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/modern-ciphers-3.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/modern-ciphers-3.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/modern-ciphers-3.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/modern-ciphers-3.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/modern-ciphers.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/modern-ciphers.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/modern-ciphers.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/modern-ciphers.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/prng-attacks.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/prng-attacks.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/prng-attacks.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/prng-attacks.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/prng.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/prng.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/prng.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/prng.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/rsa-attacks-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/rsa-attacks-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/rsa-attacks-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/rsa-attacks-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/rsa-attacks.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/rsa-attacks.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/rsa-attacks.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/rsa-attacks.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/stream-ciphers.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/stream-ciphers.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/stream-ciphers.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/stream-ciphers.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/zkp-and-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/zkp-and-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/crypto/zkp-and-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/crypto/zkp-and-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/3d-printing.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/3d-printing.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/3d-printing.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/3d-printing.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/disk-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/disk-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/disk-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/disk-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/disk-and-memory.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/disk-and-memory.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/disk-and-memory.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/disk-and-memory.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/disk-recovery.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/disk-recovery.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/disk-recovery.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/disk-recovery.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/linux-forensics.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/linux-forensics.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/linux-forensics.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/linux-forensics.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/network-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/network-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/network-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/network-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/network.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/network.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/network.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/network.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/peripheral-capture.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/peripheral-capture.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/peripheral-capture.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/peripheral-capture.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/signals-and-hardware.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/signals-and-hardware.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/signals-and-hardware.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/signals-and-hardware.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/steganography.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/steganography.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/steganography.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/steganography.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/stego-advanced-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/stego-advanced-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/stego-advanced-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/stego-advanced-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/stego-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/stego-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/stego-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/stego-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/stego-image.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/stego-image.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/stego-image.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/stego-image.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/windows.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/windows.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/forensics/windows.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/forensics/windows.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/malware/c2-and-protocols.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/malware/c2-and-protocols.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/malware/c2-and-protocols.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/malware/c2-and-protocols.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/malware/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/malware/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/malware/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/malware/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/malware/pe-and-dotnet.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/malware/pe-and-dotnet.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/malware/pe-and-dotnet.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/malware/pe-and-dotnet.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/malware/scripts-and-obfuscation.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/malware/scripts-and-obfuscation.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/malware/scripts-and-obfuscation.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/malware/scripts-and-obfuscation.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/bashjails.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/bashjails.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/bashjails.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/bashjails.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/ctfd-navigation.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/ctfd-navigation.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/ctfd-navigation.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/ctfd-navigation.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/dns.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/dns.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/dns.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/dns.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/encodings-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/encodings-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/encodings-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/encodings-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/encodings.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/encodings.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/encodings.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/encodings.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms-3.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms-3.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms-3.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms-3.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms-4.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms-4.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms-4.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms-4.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/games-and-vms.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/games-and-vms.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/linux-privesc.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/linux-privesc.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/linux-privesc.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/linux-privesc.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/pyjails.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/pyjails.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/pyjails.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/pyjails.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/misc/rf-sdr.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/misc/rf-sdr.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/misc/rf-sdr.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/misc/rf-sdr.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/osint/geolocation-and-media.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/osint/geolocation-and-media.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/osint/geolocation-and-media.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/osint/geolocation-and-media.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/osint/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/osint/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/osint/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/osint/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/osint/social-media.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/osint/social-media.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/osint/social-media.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/osint/social-media.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/osint/web-and-dns.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/osint/web-and-dns.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/osint/web-and-dns.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/osint/web-and-dns.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-3.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-3.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-3.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-3.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-4.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-4.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-4.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-4.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-5.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-5.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits-5.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits-5.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced-exploits.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced-exploits.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/field-notes.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/field-notes.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/field-notes.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/field-notes.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/format-string.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/format-string.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/format-string.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/format-string.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/heap-fsop.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/heap-fsop.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/heap-fsop.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/heap-fsop.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/heap-techniques-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/heap-techniques-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/heap-techniques-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/heap-techniques-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/heap-techniques.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/heap-techniques.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/heap-techniques.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/heap-techniques.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/kernel-bypass.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/kernel-bypass.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/kernel-bypass.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/kernel-bypass.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/kernel-techniques.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/kernel-techniques.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/kernel-techniques.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/kernel-techniques.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/kernel.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/kernel.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/kernel.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/kernel.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/overflow-basics.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/overflow-basics.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/overflow-basics.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/overflow-basics.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/rop-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/rop-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/rop-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/rop-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/rop-and-shellcode.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/rop-and-shellcode.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/rop-and-shellcode.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/rop-and-shellcode.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/sandbox-escape.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/sandbox-escape.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/pwn/sandbox-escape.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/pwn/sandbox-escape.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/anti-analysis-ctf.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/anti-analysis-ctf.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/anti-analysis-ctf.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/anti-analysis-ctf.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/anti-analysis.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/anti-analysis.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/anti-analysis.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/anti-analysis.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/field-notes.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/field-notes.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/field-notes.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/field-notes.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/languages-compiled.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/languages-compiled.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/languages-compiled.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/languages-compiled.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/languages-platforms.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/languages-platforms.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/languages-platforms.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/languages-platforms.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/languages.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/languages.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/languages.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/languages.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-ctf-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-ctf-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-ctf-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-ctf-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-ctf-3.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-ctf-3.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-ctf-3.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-ctf-3.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-ctf.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-ctf.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-ctf.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-ctf.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-runtime.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-runtime.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns-runtime.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns-runtime.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/patterns.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/patterns.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/platforms-hardware.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/platforms-hardware.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/platforms-hardware.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/platforms-hardware.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/platforms.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/platforms.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/platforms.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/platforms.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-advanced-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-advanced-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-advanced-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-advanced-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-dynamic.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-dynamic.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-dynamic.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-dynamic.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-emulation.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-emulation.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools-emulation.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools-emulation.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/reverse/tools.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/reverse/tools.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-and-access-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-and-access-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-and-access-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-and-access-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-and-access.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-and-access.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-and-access.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-and-access.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-infra.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-infra.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-infra.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-infra.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-jwt.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-jwt.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/auth-jwt.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/auth-jwt.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/client-side-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/client-side-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/client-side-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/client-side-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/client-side.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/client-side.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/client-side.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/client-side.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/cves.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/cves.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/cves.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/cves.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/field-notes.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/field-notes.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/field-notes.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/field-notes.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/node-and-prototype.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/node-and-prototype.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/node-and-prototype.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/node-and-prototype.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced-3.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced-3.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced-3.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced-3.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced-4.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced-4.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced-4.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced-4.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-advanced.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-advanced.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-deser.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-deser.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-deser.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-deser.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-exec-2.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-exec-2.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-exec-2.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-exec-2.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-exec.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-exec.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side-exec.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side-exec.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/server-side.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/server-side.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/sql-injection.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/sql-injection.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/sql-injection.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/sql-injection.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/web/web3.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/web/web3.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/web/web3.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/web/web3.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/ctf/writeup/index.md b/backend/internal/promptskills/registry/seed/tree/references/ctf/writeup/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/ctf/writeup/index.md rename to backend/internal/promptskills/registry/seed/tree/references/ctf/writeup/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/environment-and-resources.md b/backend/internal/promptskills/registry/seed/tree/references/environment-and-resources.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/environment-and-resources.md rename to backend/internal/promptskills/registry/seed/tree/references/environment-and-resources.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/evidence-workflow.md b/backend/internal/promptskills/registry/seed/tree/references/evidence-workflow.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/evidence-workflow.md rename to backend/internal/promptskills/registry/seed/tree/references/evidence-workflow.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/experience-index.md b/backend/internal/promptskills/registry/seed/tree/references/experience-index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/experience-index.md rename to backend/internal/promptskills/registry/seed/tree/references/experience-index.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/experience/deobfuscation.md b/backend/internal/promptskills/registry/seed/tree/references/experience/deobfuscation.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/experience/deobfuscation.md rename to backend/internal/promptskills/registry/seed/tree/references/experience/deobfuscation.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/experience/pitfalls.md b/backend/internal/promptskills/registry/seed/tree/references/experience/pitfalls.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/experience/pitfalls.md rename to backend/internal/promptskills/registry/seed/tree/references/experience/pitfalls.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/experience/reverse-unpacking.md b/backend/internal/promptskills/registry/seed/tree/references/experience/reverse-unpacking.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/experience/reverse-unpacking.md rename to backend/internal/promptskills/registry/seed/tree/references/experience/reverse-unpacking.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/experience/web-captcha.md b/backend/internal/promptskills/registry/seed/tree/references/experience/web-captcha.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/experience/web-captcha.md rename to backend/internal/promptskills/registry/seed/tree/references/experience/web-captcha.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/field-journal-template.md b/backend/internal/promptskills/registry/seed/tree/references/field-journal-template.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/field-journal-template.md rename to backend/internal/promptskills/registry/seed/tree/references/field-journal-template.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/malware.md b/backend/internal/promptskills/registry/seed/tree/references/malware.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/malware.md rename to backend/internal/promptskills/registry/seed/tree/references/malware.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/pentest.md b/backend/internal/promptskills/registry/seed/tree/references/pentest.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/pentest.md rename to backend/internal/promptskills/registry/seed/tree/references/pentest.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/precedent-auth.md b/backend/internal/promptskills/registry/seed/tree/references/precedent-auth.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/precedent-auth.md rename to backend/internal/promptskills/registry/seed/tree/references/precedent-auth.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/progressive-reasoning.md b/backend/internal/promptskills/registry/seed/tree/references/progressive-reasoning.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/progressive-reasoning.md rename to backend/internal/promptskills/registry/seed/tree/references/progressive-reasoning.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/reporting.md b/backend/internal/promptskills/registry/seed/tree/references/reporting.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/reporting.md rename to backend/internal/promptskills/registry/seed/tree/references/reporting.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/reverse.md b/backend/internal/promptskills/registry/seed/tree/references/reverse.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/reverse.md rename to backend/internal/promptskills/registry/seed/tree/references/reverse.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/routing.md b/backend/internal/promptskills/registry/seed/tree/references/routing.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/routing.md rename to backend/internal/promptskills/registry/seed/tree/references/routing.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/scanner.md b/backend/internal/promptskills/registry/seed/tree/references/scanner.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/scanner.md rename to backend/internal/promptskills/registry/seed/tree/references/scanner.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/scope-and-evidence.md b/backend/internal/promptskills/registry/seed/tree/references/scope-and-evidence.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/scope-and-evidence.md rename to backend/internal/promptskills/registry/seed/tree/references/scope-and-evidence.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/source-provenance.md b/backend/internal/promptskills/registry/seed/tree/references/source-provenance.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/source-provenance.md rename to backend/internal/promptskills/registry/seed/tree/references/source-provenance.md diff --git a/plugins/prompt-skills/registry/seed/tree/references/tools.md b/backend/internal/promptskills/registry/seed/tree/references/tools.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/references/tools.md rename to backend/internal/promptskills/registry/seed/tree/references/tools.md diff --git a/plugins/prompt-skills/registry/seed/tree/schemas/research-result.schema.json b/backend/internal/promptskills/registry/seed/tree/schemas/research-result.schema.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/schemas/research-result.schema.json rename to backend/internal/promptskills/registry/seed/tree/schemas/research-result.schema.json diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/env_probe.py b/backend/internal/promptskills/registry/seed/tree/scripts/env_probe.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/env_probe.py rename to backend/internal/promptskills/registry/seed/tree/scripts/env_probe.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/artifact_inventory.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/artifact_inventory.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/artifact_inventory.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/artifact_inventory.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/har_summary.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/har_summary.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/har_summary.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/har_summary.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/new-experience-entry.ps1 b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/new-experience-entry.ps1 similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/new-experience-entry.ps1 rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/new-experience-entry.ps1 diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/new_experience_entry.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/new_experience_entry.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/new_experience_entry.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/new_experience_entry.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/pack_cloud_handoff.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/pack_cloud_handoff.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/pack_cloud_handoff.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/pack_cloud_handoff.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/pe_entropy_triage.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/pe_entropy_triage.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/pe_entropy_triage.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/pe_entropy_triage.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/route_task.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/route_task.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/route_task.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/route_task.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/reusable/scaffold_project.py b/backend/internal/promptskills/registry/seed/tree/scripts/reusable/scaffold_project.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/reusable/scaffold_project.py rename to backend/internal/promptskills/registry/seed/tree/scripts/reusable/scaffold_project.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/validate_result.py b/backend/internal/promptskills/registry/seed/tree/scripts/validate_result.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/validate_result.py rename to backend/internal/promptskills/registry/seed/tree/scripts/validate_result.py diff --git a/plugins/prompt-skills/registry/seed/tree/scripts/validate_skill.py b/backend/internal/promptskills/registry/seed/tree/scripts/validate_skill.py similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/scripts/validate_skill.py rename to backend/internal/promptskills/registry/seed/tree/scripts/validate_skill.py diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/_llm-security-workflow.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/_llm-security-workflow.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/_llm-security-workflow.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/_llm-security-workflow.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-obedience-engineering.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-obedience-engineering.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-obedience-engineering.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-obedience-engineering.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-security-testing.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-security-testing.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-security-testing.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/agent-security-testing.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/owasp-llm-top10.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/owasp-llm-top10.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/owasp-llm-top10.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/owasp-llm-top10.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/prompt-injection-methodology.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/prompt-injection-methodology.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/prompt-injection-methodology.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-ai-security/references/llm-deep/prompt-injection-methodology.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/payloads/README.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/payloads/README.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/payloads/README.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/payloads/README.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentest-agents.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentest-agents.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentest-agents.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentest-agents.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentesting-landscape-2026.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentesting-landscape-2026.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentesting-landscape-2026.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/ai-pentesting-landscape-2026.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/automation-loop-pattern.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/automation-loop-pattern.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/automation-loop-pattern.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/automation-loop-pattern.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/awesome-pentest-digest.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/awesome-pentest-digest.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/awesome-pentest-digest.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/awesome-pentest-digest.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/burpsuite-mcp-guide.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/burpsuite-mcp-guide.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/burpsuite-mcp-guide.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/burpsuite-mcp-guide.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/kali-mcp-ecosystem.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/kali-mcp-ecosystem.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/kali-mcp-ecosystem.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/kali-mcp-ecosystem.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/msf-protocol.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/msf-protocol.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/msf-protocol.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/msf-protocol.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/network-attack-defense.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/network-attack-defense.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/network-attack-defense.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/network-attack-defense.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/nuclei-guide-2026.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/nuclei-guide-2026.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/nuclei-guide-2026.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/nuclei-guide-2026.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-ai-agents-matrix.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-ai-agents-matrix.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-ai-agents-matrix.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-ai-agents-matrix.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-loop.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-loop.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-loop.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/pentest-loop.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/web-attack-cheatsheet.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/web-attack-cheatsheet.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/web-attack-cheatsheet.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/references/web-attack-cheatsheet.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.claude-plugin/marketplace.json b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.claude-plugin/marketplace.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.claude-plugin/marketplace.json rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.claude-plugin/marketplace.json diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.gitignore b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.gitignore similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.gitignore rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/.gitignore diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/LICENSE b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/LICENSE similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/LICENSE rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/LICENSE diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.en.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.en.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.en.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.en.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/README.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/00-index.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/00-index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/00-index.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/00-index.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/chinese-srcfingerprints.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/chinese-srcfingerprints.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/chinese-srcfingerprints.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/chinese-srcfingerprints.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/default-credentials-cn.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/default-credentials-cn.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/default-credentials-cn.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/dictionaries/default-credentials-cn.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/00-index.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/00-index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/00-index.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/00-index.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/banking-finance.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/banking-finance.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/banking-finance.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/banking-finance.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/telecom-isp.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/telecom-isp.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/telecom-isp.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/industry/telecom-isp.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/00-index.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/00-index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/00-index.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/00-index.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/01-attack-priority.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/01-attack-priority.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/01-attack-priority.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/01-attack-priority.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/02-bypass-toolkit.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/02-bypass-toolkit.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/02-bypass-toolkit.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/02-bypass-toolkit.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/03-evidence-discipline.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/03-evidence-discipline.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/03-evidence-discipline.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/03-evidence-discipline.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/04-control-gap-hunting.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/04-control-gap-hunting.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/04-control-gap-hunting.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/04-control-gap-hunting.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/05-srctimebox-priority.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/05-srctimebox-priority.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/05-srctimebox-priority.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/methodology/05-srctimebox-priority.md diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/adcs\346\224\273\345\207\273.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/adcs\346\224\273\345\207\273.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/adcs\346\224\273\345\207\273.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/adcs\346\224\273\345\207\273.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/exchange\346\224\273\345\207\273.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/exchange\346\224\273\345\207\273.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/exchange\346\224\273\345\207\273.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/exchange\346\224\273\345\207\273.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/sharepoint\346\224\273\345\207\273.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/sharepoint\346\224\273\345\207\273.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/sharepoint\346\224\273\345\207\273.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/sharepoint\346\224\273\345\207\273.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\344\277\241\346\201\257\346\224\266\351\233\206.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\344\277\241\346\201\257\346\224\266\351\233\206.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\344\277\241\346\201\257\346\224\266\351\233\206.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\344\277\241\346\201\257\346\224\266\351\233\206.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\205\215\346\235\200\344\270\216\350\247\204\351\201\277.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\205\215\346\235\200\344\270\216\350\247\204\351\201\277.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\205\215\346\235\200\344\270\216\350\247\204\351\201\277.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\205\215\346\235\200\344\270\216\350\247\204\351\201\277.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\207\255\350\257\201\347\252\203\345\217\226.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\207\255\350\257\201\347\252\203\345\217\226.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\207\255\350\257\201\347\252\203\345\217\226.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\207\255\350\257\201\347\252\203\345\217\226.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\237\237\346\270\227\351\200\217\346\224\273\345\207\273.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\237\237\346\270\227\351\200\217\346\224\273\345\207\273.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\237\237\346\270\227\351\200\217\346\224\273\345\207\273.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\345\237\237\346\270\227\351\200\217\346\224\273\345\207\273.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\346\217\220\345\215\207.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\346\217\220\345\215\207.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\346\217\220\345\215\207.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\346\217\220\345\215\207.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\347\273\264\346\214\201.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\347\273\264\346\214\201.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\347\273\264\346\214\201.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\235\203\351\231\220\347\273\264\346\214\201.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\250\252\345\220\221\347\247\273\345\212\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\250\252\345\220\221\347\247\273\345\212\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\250\252\345\220\221\347\247\273\345\212\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\346\250\252\345\220\221\347\247\273\345\212\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\351\232\247\351\201\223\344\273\243\347\220\206.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\351\232\247\351\201\223\344\273\243\347\220\206.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\351\232\247\351\201\223\344\273\243\347\220\206.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/intranet/\351\232\247\351\201\223\344\273\243\347\220\206.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ai\345\256\211\345\205\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ai\345\256\211\345\205\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ai\345\256\211\345\205\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ai\345\256\211\345\205\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/api\345\256\211\345\205\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/api\345\256\211\345\205\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/api\345\256\211\345\205\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/api\345\256\211\345\205\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/csrf\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/csrf\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/csrf\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/csrf\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/jwt\345\256\211\345\205\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/jwt\345\256\211\345\205\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/jwt\345\256\211\345\205\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/jwt\345\256\211\345\205\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/lfi-rfi\346\226\207\344\273\266\345\214\205\345\220\253.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/lfi-rfi\346\226\207\344\273\266\345\214\205\345\220\253.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/lfi-rfi\346\226\207\344\273\266\345\214\205\345\220\253.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/lfi-rfi\346\226\207\344\273\266\345\214\205\345\220\253.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/rce\350\277\234\347\250\213\344\273\243\347\240\201\346\211\247\350\241\214.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/rce\350\277\234\347\250\213\344\273\243\347\240\201\346\211\247\350\241\214.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/rce\350\277\234\347\250\213\344\273\243\347\240\201\346\211\247\350\241\214.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/rce\350\277\234\347\250\213\344\273\243\347\240\201\346\211\247\350\241\214.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/sql-nosql\346\263\250\345\205\245.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/sql-nosql\346\263\250\345\205\245.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/sql-nosql\346\263\250\345\205\245.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/sql-nosql\346\263\250\345\205\245.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssrf\346\234\215\345\212\241\347\253\257\350\257\267\346\261\202\344\274\252\351\200\240.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssrf\346\234\215\345\212\241\347\253\257\350\257\267\346\261\202\344\274\252\351\200\240.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssrf\346\234\215\345\212\241\347\253\257\350\257\267\346\261\202\344\274\252\351\200\240.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssrf\346\234\215\345\212\241\347\253\257\350\257\267\346\261\202\344\274\252\351\200\240.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssti\346\250\241\346\235\277\346\263\250\345\205\245.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssti\346\250\241\346\235\277\346\263\250\345\205\245.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssti\346\250\241\346\235\277\346\263\250\345\205\245.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/ssti\346\250\241\346\235\277\346\263\250\345\205\245.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/websocket\345\256\211\345\205\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/websocket\345\256\211\345\205\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/websocket\345\256\211\345\205\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/websocket\345\256\211\345\205\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xss\350\267\250\347\253\231\350\204\232\346\234\254.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xss\350\267\250\347\253\231\350\204\232\346\234\254.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xss\350\267\250\347\253\231\350\204\232\346\234\254.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xss\350\267\250\347\253\231\350\204\232\346\234\254.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xxe\345\256\236\344\275\223\346\263\250\345\205\245.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xxe\345\256\236\344\275\223\346\263\250\345\205\245.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xxe\345\256\236\344\275\223\346\263\250\345\205\245.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/xxe\345\256\236\344\275\223\346\263\250\345\205\245.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\270\232\345\212\241\351\200\273\350\276\221\346\274\217\346\264\236.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\270\232\345\212\241\351\200\273\350\276\221\346\274\217\346\264\236.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\270\232\345\212\241\351\200\273\350\276\221\346\274\217\346\264\236.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\270\232\345\212\241\351\200\273\350\276\221\346\274\217\346\264\236.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\272\221\345\256\211\345\205\250\346\274\217\346\264\236.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\272\221\345\256\211\345\205\250\346\274\217\346\264\236.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\272\221\345\256\211\345\205\250\346\274\217\346\264\236.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\272\221\345\256\211\345\205\250\346\274\217\346\264\236.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\276\233\345\272\224\351\223\276\346\224\273\345\207\273.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\276\233\345\272\224\351\223\276\346\224\273\345\207\273.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\276\233\345\272\224\351\223\276\346\224\273\345\207\273.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\344\276\233\345\272\224\351\223\276\346\224\273\345\207\273.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\216\237\345\236\213\351\223\276\346\261\241\346\237\223.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\216\237\345\236\213\351\223\276\346\261\241\346\237\223.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\216\237\345\236\213\351\223\276\346\261\241\346\237\223.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\216\237\345\236\213\351\223\276\346\261\241\346\237\223.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\274\200\346\224\276\351\207\215\345\256\232\345\220\221.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\274\200\346\224\276\351\207\215\345\256\232\345\220\221.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\274\200\346\224\276\351\207\215\345\256\232\345\220\221.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\345\274\200\346\224\276\351\207\215\345\256\232\345\220\221.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\226\207\344\273\266\346\274\217\346\264\236.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\226\207\344\273\266\346\274\217\346\264\236.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\226\207\344\273\266\346\274\217\346\264\236.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\226\207\344\273\266\346\274\217\346\264\236.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\241\206\346\236\266\346\274\217\346\264\236.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\241\206\346\236\266\346\274\217\346\264\236.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\241\206\346\236\266\346\274\217\346\264\236.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\346\241\206\346\236\266\346\274\217\346\264\236.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\202\271\345\207\273\345\212\253\346\214\201.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\202\271\345\207\273\345\212\253\346\214\201.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\202\271\345\207\273\345\212\253\346\214\201.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\202\271\345\207\273\345\212\253\346\214\201.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\274\223\345\255\230\344\270\216cdn\345\256\211\345\205\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\274\223\345\255\230\344\270\216cdn\345\256\211\345\205\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\274\223\345\255\230\344\270\216cdn\345\256\211\345\205\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\347\274\223\345\255\230\344\270\216cdn\345\256\211\345\205\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\256\244\350\257\201\346\274\217\346\264\236.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\256\244\350\257\201\346\274\217\346\264\236.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\256\244\350\257\201\346\274\217\346\264\236.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\256\244\350\257\201\346\274\217\346\264\236.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\257\267\346\261\202\350\265\260\347\247\201.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\257\267\346\261\202\350\265\260\347\247\201.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\257\267\346\261\202\350\265\260\347\247\201.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/by-category/web/\350\257\267\346\261\202\350\265\260\347\247\201.md" diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/index.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/index.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/index.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/navigation.json b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/navigation.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/navigation.json rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/navigation.json diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/tools.json b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/tools.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/tools.json rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/tools.json diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/web.json b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/web.json similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/web.json rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/raw/web.json diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/web\346\270\227\351\200\217.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/web\346\270\227\351\200\217.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/web\346\270\227\351\200\217.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/web\346\270\227\351\200\217.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/windows\346\270\227\351\200\217.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/windows\346\270\227\351\200\217.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/windows\346\270\227\351\200\217.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/windows\346\270\227\351\200\217.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\344\277\241\346\201\257\346\224\266\351\233\206.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\344\277\241\346\201\257\346\224\266\351\233\206.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\344\277\241\346\201\257\346\224\266\351\233\206.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\344\277\241\346\201\257\346\224\266\351\233\206.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\206\205\347\275\221\346\270\227\351\200\217.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\206\205\347\275\221\346\270\227\351\200\217.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\206\205\347\275\221\346\270\227\351\200\217.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\206\205\347\275\221\346\270\227\351\200\217.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\207\255\350\257\201\347\252\203\345\217\226.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\207\255\350\257\201\347\252\203\345\217\226.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\207\255\350\257\201\347\252\203\345\217\226.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\207\255\350\257\201\347\252\203\345\217\226.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\237\237\346\270\227\351\200\217.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\237\237\346\270\227\351\200\217.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\237\237\346\270\227\351\200\217.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\237\237\346\270\227\351\200\217.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\257\206\347\240\201\346\224\273\345\207\273.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\257\206\347\240\201\346\224\273\345\207\273.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\257\206\347\240\201\346\224\273\345\207\273.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\345\257\206\347\240\201\346\224\273\345\207\273.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\235\203\351\231\220\346\217\220\345\215\207.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\235\203\351\231\220\346\217\220\345\215\207.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\235\203\351\231\220\346\217\220\345\215\207.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\235\203\351\231\220\346\217\220\345\215\207.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\274\217\346\264\236\345\210\251\347\224\250.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\274\217\346\264\236\345\210\251\347\224\250.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\274\217\346\264\236\345\210\251\347\224\250.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\346\274\217\346\264\236\345\210\251\347\224\250.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\263\273\347\273\237\345\221\275\344\273\244.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\263\273\347\273\237\345\221\275\344\273\244.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\263\273\347\273\237\345\221\275\344\273\244.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\263\273\347\273\237\345\221\275\344\273\244.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\272\242\351\230\237\345\267\245\345\205\267.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\272\242\351\230\237\345\267\245\345\205\267.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\272\242\351\230\237\345\267\245\345\205\267.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\272\242\351\230\237\345\267\245\345\205\267.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\274\226\347\240\201\350\247\243\347\240\201.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\274\226\347\240\201\350\247\243\347\240\201.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\274\226\347\240\201\350\247\243\347\240\201.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\347\274\226\347\240\201\350\247\243\347\240\201.md" diff --git "a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\351\232\247\351\201\223\344\273\243\347\220\206.md" "b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\351\232\247\351\201\223\344\273\243\347\220\206.md" similarity index 100% rename from "plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\351\232\247\351\201\223\344\273\243\347\220\206.md" rename to "backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/payloader/tools/\351\232\247\351\201\223\344\273\243\347\220\206.md" diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/00-index.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/00-index.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/00-index.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/00-index.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/api-rest.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/api-rest.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/api-rest.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/api-rest.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/arbitrary-x-authz.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/arbitrary-x-authz.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/arbitrary-x-authz.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/arbitrary-x-authz.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/dos.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/dos.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/dos.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/dos.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/file-upload.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/file-upload.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/file-upload.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/file-upload.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/graphql.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/graphql.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/graphql.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/graphql.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/http-smuggling.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/http-smuggling.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/http-smuggling.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/http-smuggling.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/info-disclosure.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/info-disclosure.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/info-disclosure.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/info-disclosure.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/intranet-postexp.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/intranet-postexp.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/intranet-postexp.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/intranet-postexp.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/llm-prompt-injection.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/llm-prompt-injection.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/llm-prompt-injection.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/llm-prompt-injection.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/logic-flaws.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/logic-flaws.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/logic-flaws.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/logic-flaws.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/mobile.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/mobile.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/mobile.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/mobile.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/oauth-saml-jwt.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/oauth-saml-jwt.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/oauth-saml-jwt.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/oauth-saml-jwt.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/path-traversal.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/path-traversal.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/path-traversal.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/path-traversal.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/race-conditions.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/race-conditions.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/race-conditions.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/race-conditions.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/rce.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/rce.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/rce.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/rce.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/sqli.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/sqli.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/sqli.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/sqli.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/ssrf-cache-host.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/ssrf-cache-host.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/ssrf-cache-host.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/ssrf-cache-host.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/unauth-access.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/unauth-access.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/unauth-access.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/unauth-access.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/xss.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/xss.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/xss.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/playbooks/xss.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/templates/report-submission.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/templates/report-submission.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/templates/report-submission.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/templates/report-submission.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/tools/mcp-jshook.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/tools/mcp-jshook.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/tools/mcp-jshook.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/src-hunter/references/tools/mcp-jshook.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/findings.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/findings.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/findings.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/findings.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/progress.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/progress.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/progress.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/progress.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/rules.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/rules.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/rules.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/rules.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/scope.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/scope.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/scope.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/scope.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/task_plan.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/task_plan.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/task_plan.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-assessment-tooling/pentest-tools/templates/task_plan.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-attack-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-attack-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-attack-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-attack-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-attack-chain/references/attack-playbooks.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-attack-chain/references/attack-playbooks.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-attack-chain/references/attack-playbooks.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-attack-chain/references/attack-playbooks.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-attack-chain/references/evasion-cheatsheet.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-attack-chain/references/evasion-cheatsheet.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-attack-chain/references/evasion-cheatsheet.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-attack-chain/references/evasion-cheatsheet.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-crypto/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-crypto/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-crypto/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-crypto/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/references/hook-survey.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/references/hook-survey.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/references/hook-survey.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/references/hook-survey.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/references/telemetry-blinding.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/references/telemetry-blinding.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/references/telemetry-blinding.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/references/telemetry-blinding.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/references/unhook-techniques.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/references/unhook-techniques.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-edr-bypass/references/unhook-techniques.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-edr-bypass/references/unhook-techniques.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/references/emba-automated-analysis.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/references/emba-automated-analysis.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/references/emba-automated-analysis.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/references/emba-automated-analysis.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/references/emulation-and-fuzz.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/references/emulation-and-fuzz.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/references/emulation-and-fuzz.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/references/emulation-and-fuzz.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/references/extraction-methodology.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/references/extraction-methodology.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-firmware/references/extraction-methodology.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-firmware/references/extraction-methodology.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-forensics-dfir/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-forensics-dfir/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-forensics-dfir/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-forensics-dfir/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-malware/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-malware/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-malware/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-malware/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/_malware-workflow.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/_malware-workflow.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/_malware-workflow.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/_malware-workflow.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/anti-analysis-techniques.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/anti-analysis-techniques.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/anti-analysis-techniques.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/anti-analysis-techniques.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/sandbox-orchestration.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/sandbox-orchestration.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/sandbox-orchestration.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/sandbox-orchestration.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/yara-sigma-rules.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/yara-sigma-rules.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-malware/references/malware-deep/yara-sigma-rules.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-malware/references/malware-deep/yara-sigma-rules.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-misc/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-misc/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-misc/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-misc/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-osint/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-osint/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-osint/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-osint/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/references/diff-tools-comparison.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/references/diff-tools-comparison.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/references/diff-tools-comparison.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/references/diff-tools-comparison.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/references/patch-tuesday-workflow.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/references/patch-tuesday-workflow.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/references/patch-tuesday-workflow.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/references/patch-tuesday-workflow.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/references/root-cause-and-poc.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/references/root-cause-and-poc.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-patch-diff/references/root-cause-and-poc.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-patch-diff/references/root-cause-and-poc.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/references/heap-pwn.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/references/heap-pwn.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/references/heap-pwn.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/references/heap-pwn.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/references/kernel-pwn.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/references/kernel-pwn.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/references/kernel-pwn.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/references/kernel-pwn.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/references/stack-pwn.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/references/stack-pwn.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-chain/references/stack-pwn.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-chain/references/stack-pwn.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-native/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-native/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-pwn-native/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-pwn-native/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-reporting/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-reporting/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-reporting/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-reporting/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-reverse/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-reverse/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-reverse/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-reverse/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-supply-chain/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-supply-chain/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-supply-chain/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-supply-chain/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-supply-chain/references/cicd-pipeline-security.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-supply-chain/references/cicd-pipeline-security.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-supply-chain/references/cicd-pipeline-security.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-supply-chain/references/cicd-pipeline-security.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-supply-chain/references/sbom-sca-methodology.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-supply-chain/references/sbom-sca-methodology.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-supply-chain/references/sbom-sca-methodology.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-supply-chain/references/sbom-sca-methodology.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/INSTRUCTIONS.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/INSTRUCTIONS.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/INSTRUCTIONS.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/INSTRUCTIONS.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/references/api-deep/_api-security-workflow.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/references/api-deep/_api-security-workflow.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/references/api-deep/_api-security-workflow.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/references/api-deep/_api-security-workflow.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/references/api-deep/jwt-oauth-testing.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/references/api-deep/jwt-oauth-testing.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/references/api-deep/jwt-oauth-testing.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/references/api-deep/jwt-oauth-testing.md diff --git a/plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/references/api-deep/rest-graphql-testing.md b/backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/references/api-deep/rest-graphql-testing.md similarity index 100% rename from plugins/prompt-skills/registry/seed/tree/skills/sec-web-api/references/api-deep/rest-graphql-testing.md rename to backend/internal/promptskills/registry/seed/tree/skills/sec-web-api/references/api-deep/rest-graphql-testing.md diff --git a/plugins/prompt-skills/registry/support.go b/backend/internal/promptskills/registry/support.go similarity index 100% rename from plugins/prompt-skills/registry/support.go rename to backend/internal/promptskills/registry/support.go diff --git a/plugins/prompt-skills/registry/validation.go b/backend/internal/promptskills/registry/validation.go similarity index 100% rename from plugins/prompt-skills/registry/validation.go rename to backend/internal/promptskills/registry/validation.go diff --git a/plugins/prompt-skills/source/gpt56.go b/backend/internal/promptskills/source/gpt56.go similarity index 100% rename from plugins/prompt-skills/source/gpt56.go rename to backend/internal/promptskills/source/gpt56.go diff --git a/plugins/prompt-skills/source/gpt56_test.go b/backend/internal/promptskills/source/gpt56_test.go similarity index 100% rename from plugins/prompt-skills/source/gpt56_test.go rename to backend/internal/promptskills/source/gpt56_test.go diff --git a/plugins/prompt-skills/source/prompts/gpt_5_6_instruct_v45.md b/backend/internal/promptskills/source/prompts/gpt_5_6_instruct_v45.md similarity index 100% rename from plugins/prompt-skills/source/prompts/gpt_5_6_instruct_v45.md rename to backend/internal/promptskills/source/prompts/gpt_5_6_instruct_v45.md diff --git a/plugins/prompt-skills/source/support.go b/backend/internal/promptskills/source/support.go similarity index 100% rename from plugins/prompt-skills/source/support.go rename to backend/internal/promptskills/source/support.go diff --git a/backend/internal/repository/business_system_prompt_repository_test.go b/backend/internal/repository/business_system_prompt_repository_test.go index 1401ba194cca..c6926eeac609 100644 --- a/backend/internal/repository/business_system_prompt_repository_test.go +++ b/backend/internal/repository/business_system_prompt_repository_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - promptsource "github.com/HTExplicit/sub2api-plugins/promptskills/source" + promptsource "github.com/Wei-Shaw/sub2api/internal/promptskills/source" "github.com/DATA-DOG/go-sqlmock" "github.com/Wei-Shaw/sub2api/internal/service" diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index cb6a687ce11c..be88aec9eb5c 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -179,52 +179,34 @@ func registerCindyBalanceProbeRoutes(admin *gin.RouterGroup, h *handler.Handlers func registerSystemPromptRoutes(admin *gin.RouterGroup, h *handler.Handlers) { prompts := admin.Group("/system-prompts") - for _, route := range []struct { - name, method, path string - handler gin.HandlerFunc - }{ - {"prompts.list", "GET", "", h.Admin.SystemPrompt.List}, - {"prompts.create", "POST", "", h.Admin.SystemPrompt.Create}, - {"prompts.runtime.read", "GET", "/runtime", h.Admin.SystemPrompt.Runtime}, - {"prompts.runtime.update", "PUT", "/runtime", h.Admin.SystemPrompt.UpdateRuntime}, - {"prompts.rules.read", "GET", "/rules", h.Admin.SystemPrompt.Rules}, - {"prompts.rules.update", "PUT", "/rules", h.Admin.SystemPrompt.UpdateRules}, - {"prompts.bindings.read", "POST", "/accounts/resolve", h.Admin.SystemPrompt.AccountBindings}, - {"prompts.bindings.update", "POST", "/accounts/bindings", h.Admin.SystemPrompt.UpdateAccountBindings}, - {"prompts.rules.preview", "POST", "/rules/preview/:account_id", h.Admin.SystemPrompt.PreviewRules}, - {"prompts.preview.merge", "POST", "/preview/merge", h.Admin.SystemPrompt.PreviewMerge}, - {"prompts.preview.upstream", "POST", "/preview/upstream", h.Admin.SystemPrompt.PreviewUpstream}, - {"skills.registry", "GET", "/skill-registry", h.Admin.SystemPrompt.SkillRegistry}, - {"skills.versions", "GET", "/skill-registry/versions", h.Admin.SystemPrompt.SkillVersions}, - {"skills.version", "GET", "/skill-registry/versions/:bundle_version_id", h.Admin.SystemPrompt.SkillVersion}, - {"skills.sync.start", "POST", "/skill-registry/syncs", h.Admin.SystemPrompt.StartSkillSync}, - {"skills.sync.read", "GET", "/skill-registry/syncs/:sync_id", h.Admin.SystemPrompt.SkillSync}, - {"skills.publish", "POST", "/skill-registry/versions/:bundle_version_id/publish", h.Admin.SystemPrompt.PublishSkillVersion}, - {"skills.rollback", "POST", "/skill-registry/versions/:bundle_version_id/rollback", h.Admin.SystemPrompt.RollbackSkillVersion}, - {"prompts.read", "GET", "/:id", h.Admin.SystemPrompt.Get}, - {"prompts.versions", "GET", "/:id/versions", h.Admin.SystemPrompt.Versions}, - {"prompts.update", "PATCH", "/:id", h.Admin.SystemPrompt.Update}, - {"prompts.delete", "DELETE", "/:id", h.Admin.SystemPrompt.Delete}, - {"prompts.duplicate", "POST", "/:id/duplicate", h.Admin.SystemPrompt.Duplicate}, - {"prompts.draft", "POST", "/:id/versions", h.Admin.SystemPrompt.SaveVersion}, - {"prompts.source.sync", "POST", "/:id/upstream-sync", h.Admin.SystemPrompt.SyncManagedSource}, - {"prompts.publish", "POST", "/:id/versions/:version_id/publish", h.Admin.SystemPrompt.Publish}, - {"prompts.rollback", "POST", "/:id/versions/:version_id/rollback", h.Admin.SystemPrompt.Rollback}, - } { - descriptor := extensionv1.ResourceDescriptor{ResourceGrant: extensionv1.ResourceGrant{Name: route.name, Capability: extensionv1.CapabilityRequest, Permission: "admin"}, Method: route.method, Path: prompts.BasePath() + route.path} - if route.name == "prompts.bindings.read" { - descriptor.AccountBodyField = "account_ids" - } - if route.name == "prompts.bindings.update" { - descriptor.AccountItemsField = "updates" - } - if route.name == "prompts.rules.preview" { - descriptor.AccountParam = "account_id" - } - if route.name == "prompts.bindings.read" || route.name == "prompts.bindings.update" || route.name == "prompts.rules.preview" { - descriptor.Capability = extensionv1.CapabilityAdmin - } - prompts.Handle(route.method, route.path, h.Admin.Plugin.RegisterResource(descriptor), route.handler) + { + prompts.GET("", h.Admin.SystemPrompt.List) + prompts.POST("", h.Admin.SystemPrompt.Create) + prompts.GET("/runtime", h.Admin.SystemPrompt.Runtime) + prompts.PUT("/runtime", h.Admin.SystemPrompt.UpdateRuntime) + prompts.GET("/rules", h.Admin.SystemPrompt.Rules) + prompts.PUT("/rules", h.Admin.SystemPrompt.UpdateRules) + prompts.POST("/accounts/resolve", h.Admin.SystemPrompt.AccountBindings) + prompts.POST("/accounts/bindings", h.Admin.SystemPrompt.UpdateAccountBindings) + prompts.POST("/rules/preview/:account_id", h.Admin.SystemPrompt.PreviewRules) + prompts.POST("/preview/merge", h.Admin.SystemPrompt.PreviewMerge) + prompts.POST("/preview/upstream", h.Admin.SystemPrompt.PreviewUpstream) + prompts.GET("/skill-registry", h.Admin.SystemPrompt.SkillRegistry) + prompts.GET("/skill-registry/versions", h.Admin.SystemPrompt.SkillVersions) + prompts.GET("/skill-registry/versions/:bundle_version_id", h.Admin.SystemPrompt.SkillVersion) + prompts.POST("/skill-registry/syncs", h.Admin.SystemPrompt.StartSkillSync) + prompts.GET("/skill-registry/syncs/:sync_id", h.Admin.SystemPrompt.SkillSync) + prompts.POST("/skill-registry/versions/:bundle_version_id/publish", h.Admin.SystemPrompt.PublishSkillVersion) + prompts.POST("/skill-registry/versions/:bundle_version_id/rollback", h.Admin.SystemPrompt.RollbackSkillVersion) + prompts.GET("/:id", h.Admin.SystemPrompt.Get) + prompts.GET("/:id/versions", h.Admin.SystemPrompt.Versions) + prompts.PATCH("/:id", h.Admin.SystemPrompt.Update) + prompts.DELETE("/:id", h.Admin.SystemPrompt.Delete) + prompts.POST("/:id/duplicate", h.Admin.SystemPrompt.Duplicate) + prompts.POST("/:id/versions", h.Admin.SystemPrompt.SaveVersion) + prompts.POST("/:id/upstream-sync", h.Admin.SystemPrompt.SyncManagedSource) + prompts.POST("/:id/versions/:version_id/publish", h.Admin.SystemPrompt.Publish) + prompts.POST("/:id/versions/:version_id/rollback", h.Admin.SystemPrompt.Rollback) } } diff --git a/backend/internal/service/business_system_prompt_account_scope_test.go b/backend/internal/service/business_system_prompt_account_scope_test.go index c100a643ddf2..78269135c705 100644 --- a/backend/internal/service/business_system_prompt_account_scope_test.go +++ b/backend/internal/service/business_system_prompt_account_scope_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - promptpolicy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" + promptpolicy "github.com/Wei-Shaw/sub2api/internal/promptskills/policy" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" @@ -17,51 +17,36 @@ import ( ) type accountScopedPromptInvoker struct { - manager *PluginManager - installation *PluginInstallation - module *promptpolicy.Module - other extensionv1.OperationInvoker - calls []extensionv1.Invocation + module *promptpolicy.Module + rollout int + calls []extensionv1.Invocation } -func (p *accountScopedPromptInvoker) InvokeOperation(ctx context.Context, platform, kind string, in extensionv1.Invocation) (extensionv1.Result, error) { - if in.Operation != "prompt.plan" && in.Operation != "prompt.availability" { - if p.other != nil { - return p.other.InvokeOperation(ctx, platform, kind, in) - } - return extensionv1.Result{}, ErrExtensionOperationDisabled - } - if in.Operation == "prompt.plan" { - p.calls = append(p.calls, in) +// allows models a per-account prompt decision: 0 denies every account, 100 +// allows every account and anything between allows odd account IDs. +// Accountless previews are always allowed. +func (p *accountScopedPromptInvoker) allows(accountID int64) bool { + switch { + case accountID == 0 || p.rollout >= 100: + return true + case p.rollout <= 0: + return false + default: + return accountID%2 == 1 } - // Use the actual host operation ownership/cohort gate, then run only the - // independent policy module in process. No customer body or model IO exists. - if _, _, err := p.manager.operationOwner(platform, kind, in); err != nil { - return extensionv1.Result{}, err - } - return p.module.Invoke(ctx, in) } -func (p *accountScopedPromptInvoker) InvokeDomainOperation(ctx context.Context, in extensionv1.Invocation, _ bool) (extensionv1.Result, error) { - if in.Operation != "prompt.availability" { - if other, ok := p.other.(domainOperationInvoker); ok { - return other.InvokeDomainOperation(ctx, in, false) - } - return p.InvokeOperation(ctx, "*", "*", in) - } - platform, kind, err := p.manager.domainOperationScope(ctx, in) - if err != nil { - return extensionv1.Result{}, err +// invoke denies prompt plans for accounts outside the modeled scope, which +// covers API-key accounts only. +func (p *accountScopedPromptInvoker) invoke(ctx context.Context, in extensionv1.Invocation) (extensionv1.Result, error) { + if in.Operation != "prompt.plan" { + return p.module.Invoke(ctx, in) } - return p.InvokeOperation(ctx, platform, kind, in) -} - -func (p *accountScopedPromptInvoker) BindDomainOperationContext(ctx context.Context, in extensionv1.Invocation) (context.Context, context.CancelFunc, error) { - if _, _, err := p.manager.domainOperationScope(ctx, in); err != nil { - return nil, nil, err + p.calls = append(p.calls, in) + if accountType := gjson.GetBytes(in.Payload, "target.account_type").String(); !p.allows(in.AccountID) || accountType != AccountTypeAPIKey { + return extensionv1.Result{}, ErrExtensionOperationDisabled } - bound, cancel := context.WithCancel(ctx) - return bound, cancel, nil + return p.module.Invoke(ctx, in) } func newAccountScopedPromptGateway(t *testing.T, rollout int) (*OpenAIGatewayService, *accountScopedPromptInvoker, *fakeBusinessSystemPromptStore) { @@ -69,37 +54,16 @@ func newAccountScopedPromptGateway(t *testing.T, rollout int) (*OpenAIGatewaySer store := &fakeBusinessSystemPromptStore{loaded: BusinessSystemPromptSnapshot{Revision: 1, Enabled: true, Body: "account-scoped-server"}} policy := NewBusinessSystemPromptService(store, nil) require.NoError(t, policy.Initialize(context.Background())) - installation := &PluginInstallation{ID: 7, State: PluginStateEnabled, - Manifest: PluginManifest{Operations: map[string][]string{extensionv1.CapabilityRequest: {"prompt.plan", "prompt.availability"}}}, - Bindings: []PluginBinding{{Capability: extensionv1.CapabilityRequest, Platform: PlatformOpenAI, AccountType: AccountTypeAPIKey, Enabled: true, RolloutPercent: rollout}}, - } - manager := NewPluginManager(nil, nil, nil, PluginHostInfo{}, nil) - manager.extensions.Store(&pluginExtensionRegistry{installations: map[int64]*PluginInstallation{installation.ID: installation}, runtimes: map[int64]*pluginRuntime{}}) - invoker := &accountScopedPromptInvoker{manager: manager, installation: installation, module: promptpolicy.New()} - previous := processExtensionOperations.Load() - if previous != nil { - invoker.other = previous.invoker - } - processExtensionOperations.Store(&extensionOperationProvider{invoker: invoker}) - t.Cleanup(func() { processExtensionOperations.Store(previous) }) + invoker := &accountScopedPromptInvoker{module: promptpolicy.New(), rollout: rollout} + previous := invokePromptSkills + invokePromptSkills = invoker.invoke + t.Cleanup(func() { invokePromptSkills = previous }) return &OpenAIGatewayService{businessPromptService: policy}, invoker, store } func promptScopeAccounts(t *testing.T) (*Account, *Account) { t.Helper() - var included, excluded *Account - for id := int64(1); id <= 1000 && (included == nil || excluded == nil); id++ { - account := &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey} - if stablePluginBucket(id) < 50 && included == nil { - included = account - } - if stablePluginBucket(id) >= 50 && excluded == nil { - excluded = account - } - } - require.NotNil(t, included) - require.NotNil(t, excluded) - return included, excluded + return &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, &Account{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey} } func TestPromptRealAccountZeroRolloutRejected(t *testing.T) { @@ -140,7 +104,7 @@ func TestPromptRealAccountRolloutAndAccountlessPreview(t *testing.T) { input := []byte(`{"instructions":"client","input":"private-customer-history"}`) _, application, err := gateway.applyBusinessSystemPrompt(input, account, BusinessSystemPromptProtocolResponses, false) require.NoError(t, err) - require.Equal(t, int(stablePluginBucket(account.ID)) < rollout, application.Applied) + require.Equal(t, invoker.allows(account.ID), application.Applied) call := invoker.calls[len(invoker.calls)-1] require.Equal(t, account.ID, call.AccountID) require.Equal(t, account.ID, gjson.GetBytes(call.Payload, "target.account_id").Int()) @@ -219,7 +183,7 @@ func TestPromptAccountChangeRestoresOnlyOwnedCarrier(t *testing.T) { } func TestPromptSameAccountCacheRechecksRevocation(t *testing.T) { - for _, revoke := range []string{"zero_rollout", "disabled"} { + for _, revoke := range []string{"denied"} { for _, form := range []string{"input", "output", "cache_key_rewrite"} { t.Run(revoke+"/"+form, func(t *testing.T) { gateway, invoker, _ := newAccountScopedPromptGateway(t, 100) @@ -236,12 +200,7 @@ func TestPromptSameAccountCacheRechecksRevocation(t *testing.T) { body, err = rewriteBusinessSystemPromptCacheKey(ctx, body, application) require.NoError(t, err) } - if revoke == "disabled" { - invoker.installation.State = PluginStateDisabled - invoker.installation.Bindings[0].Enabled = false - } else { - invoker.installation.Bindings[0].RolloutPercent = 0 - } + invoker.rollout = 0 clean, denied, err := gateway.applyBusinessSystemPromptForRequest(ctx, body, account, BusinessSystemPromptProtocolResponses, false) require.NoError(t, err) require.False(t, denied.Applied) @@ -580,7 +539,7 @@ func TestPromptCarrierTransitionCannotForgetEarlierAppliedOutput(t *testing.T) { other := &Account{ID: secondAccount.ID + 10000, Platform: PlatformGrok, Type: AccountTypeAPIKey} if destination == "denied" { other.Platform = PlatformOpenAI - invoker.installation.Bindings[0].RolloutPercent = 0 + invoker.rollout = 0 } clean, application, err := gateway.applyBusinessSystemPromptForRequest(ctx, olderOutput, other, BusinessSystemPromptProtocolChat, false) if err != nil { diff --git a/backend/internal/service/business_system_prompt_policy.go b/backend/internal/service/business_system_prompt_policy.go index 48d760f2496a..20d39be40d8c 100644 --- a/backend/internal/service/business_system_prompt_policy.go +++ b/backend/internal/service/business_system_prompt_policy.go @@ -61,14 +61,14 @@ func MergeBusinessSystemPromptInstructions(client, server string) string { } } -// ApplyBusinessSystemPromptToJSON applies the plugin's bounded policy result to -// the original request locally. Large user histories never cross the RPC limit. +// ApplyBusinessSystemPromptToJSON applies the bounded policy result to the +// original request locally. Large user histories never enter the policy input. func ApplyBusinessSystemPromptToJSON(body []byte, snapshot BusinessSystemPromptSnapshot, target BusinessSystemPromptTarget) ([]byte, BusinessSystemPromptApplication, error) { return ApplyBusinessSystemPromptToJSONContext(context.Background(), body, snapshot, target) } func ApplyBusinessSystemPromptToJSONContext(ctx context.Context, body []byte, snapshot BusinessSystemPromptSnapshot, target BusinessSystemPromptTarget) ([]byte, BusinessSystemPromptApplication, error) { - return applyBusinessSystemPromptWithInvoker(ctx, body, snapshot, target, invokeProcessExtension) + return applyBusinessSystemPromptWithInvoker(ctx, body, snapshot, target, promptPlanInvoke) } func applyBusinessSystemPromptWithInvoker(parent context.Context, body []byte, snapshot BusinessSystemPromptSnapshot, target BusinessSystemPromptTarget, invoke func(context.Context, string, string, extensionv1.Invocation) (extensionv1.Result, error)) ([]byte, BusinessSystemPromptApplication, error) { diff --git a/backend/internal/service/business_system_prompt_rules_gateway_test.go b/backend/internal/service/business_system_prompt_rules_gateway_test.go index 7e12825c678b..cbc18e53d0e2 100644 --- a/backend/internal/service/business_system_prompt_rules_gateway_test.go +++ b/backend/internal/service/business_system_prompt_rules_gateway_test.go @@ -297,7 +297,7 @@ func TestPromptRulesExecutionScopeRevocationRestoresWireAndCacheKey(t *testing.T wire, err := gateway.finalizeBusinessPromptForSend(c, account, input, "responses", false) require.NoError(t, err) require.Contains(t, string(wire), "site-rule-content") - invoker.installation.Bindings[0].RolloutPercent = 0 + invoker.rollout = 0 clean, err := gateway.finalizeBusinessPromptForSend(c, account, wire, "responses", false) require.NoError(t, err) require.JSONEq(t, string(input), string(clean)) diff --git a/backend/internal/service/business_system_prompt_rules_test.go b/backend/internal/service/business_system_prompt_rules_test.go index 0440fe727b76..441f008be722 100644 --- a/backend/internal/service/business_system_prompt_rules_test.go +++ b/backend/internal/service/business_system_prompt_rules_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - promptpolicy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" + promptpolicy "github.com/Wei-Shaw/sub2api/internal/promptskills/policy" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" diff --git a/backend/internal/service/business_system_prompt_rules_ws.go b/backend/internal/service/business_system_prompt_rules_ws.go index 0d5f5a026fca..86bf8c091214 100644 --- a/backend/internal/service/business_system_prompt_rules_ws.go +++ b/backend/internal/service/business_system_prompt_rules_ws.go @@ -21,7 +21,7 @@ func (s *OpenAIGatewayService) prepareBusinessPromptWSIngress(c *gin.Context, bo return s.applyBusinessSystemPromptForRequest(c, body, account, protocol, compact) } target := enrichPromptTarget(c, body, businessSystemPromptTargetForAccount(account, protocol, compact)) - application, err := planBusinessSystemPromptWithInvoker(promptPolicyRequestContext(c), body, snapshot, target, invokeProcessExtension) + application, err := planBusinessSystemPromptWithInvoker(promptPolicyRequestContext(c), body, snapshot, target, promptPlanInvoke) if err != nil { return nil, application, err } diff --git a/backend/internal/service/business_system_prompt_service.go b/backend/internal/service/business_system_prompt_service.go index 29cf0319427e..0497396b0183 100644 --- a/backend/internal/service/business_system_prompt_service.go +++ b/backend/internal/service/business_system_prompt_service.go @@ -207,12 +207,6 @@ func (s *BusinessSystemPromptService) SyncManagedSource( if !ok { return BusinessSystemPromptSourceSyncResult{}, ErrBusinessSystemPromptSourceUnavailable } - bound, release, err := bindProcessDomainExtensionContext(ctx, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "prompt.source.fetch"}) - if err != nil { - return BusinessSystemPromptSourceSyncResult{}, ErrBusinessSystemPromptSourceUnavailable - } - defer release() - ctx = bound candidate, err := s.source.Fetch(ctx) if err != nil { return BusinessSystemPromptSourceSyncResult{}, err @@ -233,7 +227,7 @@ func (s *BusinessSystemPromptService) Initialize(ctx context.Context) error { return errors.New("business system prompt store unavailable") } var seeds []BusinessSystemPromptSeed - if err := invokePromptManagementPolicy(ctx, "prompt.seeds", struct{}{}, &seeds, true); err != nil { + if err := invokePromptManagement(ctx, "prompt.seeds", struct{}{}, &seeds); err != nil { return err } for i := range seeds { diff --git a/backend/internal/service/openai_business_system_prompt.go b/backend/internal/service/openai_business_system_prompt.go index e6de2c6b0565..588dc903247a 100644 --- a/backend/internal/service/openai_business_system_prompt.go +++ b/backend/internal/service/openai_business_system_prompt.go @@ -480,7 +480,7 @@ func (s *OpenAIGatewayService) applyBusinessSystemPromptForRequest( if frozen.Revision < 1 && state.application.Applied { return nil, BusinessSystemPromptApplication{}, ErrBusinessSystemPromptUnavailable } - application, err := planBusinessSystemPromptWithInvoker(promptPolicyRequestContext(ctx), body, frozen, target, invokeProcessExtension) + application, err := planBusinessSystemPromptWithInvoker(promptPolicyRequestContext(ctx), body, frozen, target, promptPlanInvoke) if err != nil { return nil, BusinessSystemPromptApplication{}, err } diff --git a/backend/internal/service/plugin_domain_operation_test.go b/backend/internal/service/plugin_domain_operation_test.go index c5e0771a7aa4..f97bee80dc11 100644 --- a/backend/internal/service/plugin_domain_operation_test.go +++ b/backend/internal/service/plugin_domain_operation_test.go @@ -28,29 +28,6 @@ func scopedPromptManager(t *testing.T) *PluginManager { return manager } -func TestPromptDomainUsesActiveScopeWithoutBroadeningRequestBindings(t *testing.T) { - manager := scopedPromptManager(t) - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) - processExtensionOperations.Store(&extensionOperationProvider{invoker: manager}) - require.NoError(t, promptPolicyAvailability(context.Background())) - prompts := &BusinessSystemPromptService{} - prompts.snapshot.Store(&BusinessSystemPromptSnapshot{Enabled: true, Body: "scoped-server"}) - snapshot, ok := prompts.CurrentSnapshot() - require.True(t, ok) - require.Equal(t, "scoped-server", snapshot.Body) - name := "scoped" - plan, err := planPromptTemplate(context.Background(), extensionv1.PromptTemplatePolicyRequest{Name: &name}) - require.NoError(t, err) - require.Equal(t, name, *plan.Name) - body := []byte(`{"input":"client"}`) - out, applied, err := ApplyBusinessSystemPromptToJSONContext(context.Background(), body, snapshot, - BusinessSystemPromptTarget{Platform: PlatformOpenAI, AccountType: AccountTypeAPIKey, Protocol: BusinessSystemPromptProtocolResponses}) - require.NoError(t, err) - require.Equal(t, body, out) - require.False(t, applied.Applied, "global metadata must not broaden account request scope") -} - func TestPromptDomainAvailabilityDistinguishesDisabledFailureAndAmbiguousOwners(t *testing.T) { manager := scopedPromptManager(t) query := extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "prompt.availability", Payload: []byte(`{}`)} diff --git a/backend/internal/service/plugin_extension_runtime_test.go b/backend/internal/service/plugin_extension_runtime_test.go index 14ae7abd6d34..e6a26f159eba 100644 --- a/backend/internal/service/plugin_extension_runtime_test.go +++ b/backend/internal/service/plugin_extension_runtime_test.go @@ -68,87 +68,6 @@ func TestExtensionRuntimeUsesOfficialProcessAndHostBroker(t *testing.T) { // HTTP upstream exists in this test, so it cannot issue a real model request. } -func TestPromptExtensionRuntimeUsesIndependentProcess(t *testing.T) { - binary := os.Getenv("SUB2API_PROMPT_SKILLS_TEST_BINARY") - if binary == "" { - t.Skip("set SUB2API_PROMPT_SKILLS_TEST_BINARY to the independent prompt policy program") - } - data, err := os.ReadFile(binary) - require.NoError(t, err) - digest := sha256.Sum256(data) - installation := &PluginInstallation{ID: 9, PluginKey: "codexrip.prompt-skills", Version: firstPartyTestVersion(t, "prompt-skills"), BinaryPath: binary, BinarySHA256: hex.EncodeToString(digest[:]), Manifest: PluginManifest{Requires: PluginRequirements{ExtensionAPI: 1}, Capabilities: []PluginCapability{{ID: extensionv1.CapabilityRequest, Platform: PlatformOpenAI, AccountType: "*"}}}} - host := newPluginHostServiceServer(installation.PluginKey, nil, nil, PluginAccountScope{}) - host.extension = &pluginExtensionHost{key: installation.PluginKey, state: &extensionReadOnlyFixture{}} - socketDir := filepath.Join(t.TempDir(), "runtime") - require.NoError(t, os.MkdirAll(socketDir, 0700)) - runtime, err := startPluginRuntime(context.Background(), installation, 15*time.Second, socketDir, host) - require.NoError(t, err) - t.Cleanup(runtime.kill) - require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{}`))) - raw, _ := json.Marshal(extensionv1.PromptPlanRequest{Snapshot: BusinessSystemPromptSnapshot{Enabled: true, Body: "fixture-server"}, Target: BusinessSystemPromptTarget{Platform: PlatformOpenAI, AccountType: AccountTypeOAuth, Protocol: BusinessSystemPromptProtocolResponses}}) - out, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "prompt.plan", Payload: raw}) - require.NoError(t, err) - require.Empty(t, out.Code) - var application BusinessSystemPromptApplication - require.NoError(t, json.Unmarshal(out.Payload, &application)) - require.True(t, application.Applied) - require.Equal(t, "fixture-server", application.ServerInstructions) - index, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "skills.seed.index", Payload: []byte(`{}`)}) - require.NoError(t, err) - var manifest extensionv1.SkillManifest - require.NoError(t, json.Unmarshal(index.Payload, &manifest)) - total := 0 - for _, entry := range manifest.Files { - digest := sha256.New() - for offset := 0; offset < entry.ByteLength; { - query, _ := json.Marshal(extensionv1.SkillSeedChunkRequest{Path: entry.Path, Offset: offset, Limit: 256 << 10}) - result, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "skills.seed.chunk", Payload: query}) - require.NoError(t, err) - require.Empty(t, result.Code) - require.Less(t, len(result.Payload), extensionv1.MaxPayloadBytes) - var chunk extensionv1.SkillSeedChunk - require.NoError(t, json.Unmarshal(result.Payload, &chunk)) - require.Equal(t, entry.ByteLength, chunk.TotalBytes) - require.NotEmpty(t, chunk.Data) - _, _ = digest.Write(chunk.Data) - offset += len(chunk.Data) - total += len(chunk.Data) - } - require.Equal(t, entry.SHA256, hex.EncodeToString(digest.Sum(nil))) - } - require.Greater(t, total, extensionv1.MaxPayloadBytes, "a complete seed larger than the RPC ceiling must remain loadable") -} - -func TestCindyProviderExtensionRuntimeUsesIndependentProcess(t *testing.T) { - binary := os.Getenv("SUB2API_CINDY_PROVIDER_TEST_BINARY") - if binary == "" { - t.Skip("set SUB2API_CINDY_PROVIDER_TEST_BINARY to the independent Cindy provider program") - } - data, err := os.ReadFile(binary) - require.NoError(t, err) - digest := sha256.Sum256(data) - installation := &PluginInstallation{ID: 11, PluginKey: "codexrip.cindy-provider", Version: firstPartyTestVersion(t, "cindy-provider"), BinaryPath: binary, BinarySHA256: hex.EncodeToString(digest[:]), Manifest: PluginManifest{Requires: PluginRequirements{ExtensionAPI: 1}, Capabilities: []PluginCapability{{ID: extensionv1.CapabilityProvider, Platform: PlatformCindy, AccountType: AccountTypeAPIKey}}}} - host := newPluginHostServiceServer(installation.PluginKey, nil, nil, PluginAccountScope{}) - host.extension = &pluginExtensionHost{key: installation.PluginKey, state: &extensionReadOnlyFixture{}} - socketDir := filepath.Join(t.TempDir(), "runtime") - require.NoError(t, os.MkdirAll(socketDir, 0700)) - runtime, err := startPluginRuntime(context.Background(), installation, 15*time.Second, socketDir, host) - require.NoError(t, err) - t.Cleanup(runtime.kill) - require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{"catalog_enabled":true,"search_enabled":true}`))) - out, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityProvider, Operation: "cindy.pricing", Payload: []byte(`{}`)}) - require.NoError(t, err) - require.Empty(t, out.Code) - var snapshot extensionv1.CindyPricingSnapshot - require.NoError(t, json.Unmarshal(out.Payload, &snapshot)) - require.True(t, snapshot.Config.CatalogEnabled) - var pricing CindyTextPricing - var known bool - require.True(t, queryCindyPricingSnapshot(&snapshot, "CindyTextPricingForModel", "gpt-5.6-luna", []any{&pricing, &known})) - require.True(t, known) - require.Positive(t, pricing.InputCostPerToken) -} - func TestAccountToolsExtensionRuntimeUsesIndependentProcess(t *testing.T) { binary := os.Getenv("SUB2API_ACCOUNT_TOOLS_TEST_BINARY") if binary == "" { diff --git a/backend/internal/service/plugin_prompt_fixture_test.go b/backend/internal/service/plugin_prompt_fixture_test.go index 293122aab7b1..f5b5b3284b66 100644 --- a/backend/internal/service/plugin_prompt_fixture_test.go +++ b/backend/internal/service/plugin_prompt_fixture_test.go @@ -12,7 +12,7 @@ import ( accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" - policy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" + policy "github.com/Wei-Shaw/sub2api/internal/promptskills/policy" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) diff --git a/backend/internal/service/plugin_prompt_profile_test.go b/backend/internal/service/plugin_prompt_profile_test.go index 7ff97d6d0f91..c1d43791c33d 100644 --- a/backend/internal/service/plugin_prompt_profile_test.go +++ b/backend/internal/service/plugin_prompt_profile_test.go @@ -31,36 +31,34 @@ func TestSkillStorageRejectsNonHashPathBeforeCallingThePlugin(t *testing.T) { require.ErrorIs(t, err, ErrBusinessSystemPromptBundleInvalid) } -type restrictedSkillProfileFixture struct { - promptPolicyFixture - bytesOnly bool -} - -func (f restrictedSkillProfileFixture) InvokeOperation(ctx context.Context, platform, kind string, in extensionv1.Invocation) (extensionv1.Result, error) { - result, err := f.promptPolicyFixture.InvokeOperation(ctx, platform, kind, in) - if err != nil || in.Operation != "skills.profile" { +// restrictedSkillProfile lowers one registry profile limit returned by invoke. +func restrictedSkillProfile(invoke func(context.Context, extensionv1.Invocation) (extensionv1.Result, error), bytesOnly bool) func(context.Context, extensionv1.Invocation) (extensionv1.Result, error) { + return func(ctx context.Context, in extensionv1.Invocation) (extensionv1.Result, error) { + result, err := invoke(ctx, in) + if err != nil || in.Operation != "skills.profile" { + return result, err + } + var profile extensionv1.SkillRegistryPolicyProfile + if err = json.Unmarshal(result.Payload, &profile); err != nil { + return result, err + } + if bytesOnly { + profile.MaxTotalBytes = 1 + } else { + profile.MaxFileCount = 1 + } + result.Payload, err = json.Marshal(profile) return result, err } - var profile extensionv1.SkillRegistryPolicyProfile - if err = json.Unmarshal(result.Payload, &profile); err != nil { - return result, err - } - if f.bytesOnly { - profile.MaxTotalBytes = 1 - } else { - profile.MaxFileCount = 1 - } - result.Payload, err = json.Marshal(profile) - return result, err } func TestSkillProfileLimitsStopDownloadsBeforeAnyNetworkRequest(t *testing.T) { prompt, err := buildRemoteSkillPromptCapture([]byte(modelGangPromptCaptureFixture)) require.NoError(t, err) - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) + previous := invokePromptSkills + t.Cleanup(func() { invokePromptSkills = previous }) for _, bytesOnly := range []bool{false, true} { - processExtensionOperations.Store(&extensionOperationProvider{invoker: restrictedSkillProfileFixture{bytesOnly: bytesOnly}}) + invokePromptSkills = restrictedSkillProfile(previous, bytesOnly) client := &fakeRemoteSkillHTTPClient{} _, err := NewMoxinggangRemoteSkillCandidateSource(client).Build(context.Background(), prompt, nil) require.ErrorIs(t, err, ErrBusinessSystemPromptBundleInvalid) diff --git a/backend/internal/service/plugin_prompt_test.go b/backend/internal/service/plugin_prompt_test.go index 2ecc47475bb1..f449534da799 100644 --- a/backend/internal/service/plugin_prompt_test.go +++ b/backend/internal/service/plugin_prompt_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - policy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" + policy "github.com/Wei-Shaw/sub2api/internal/promptskills/policy" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" diff --git a/backend/internal/service/prompt_domain_runtime.go b/backend/internal/service/prompt_domain_runtime.go index 0773b5257861..d8d2a079ecb6 100644 --- a/backend/internal/service/prompt_domain_runtime.go +++ b/backend/internal/service/prompt_domain_runtime.go @@ -10,8 +10,8 @@ import ( extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) -// Initialize domain storage only after the signed policy process is ready. -// Existing state stays available for history while the plugin is disabled. +// Initialize domain storage only after the embedded skill registry is ready. +// Existing state stays available for history otherwise. type PromptDomainRuntime struct { registry *RemoteSkillRegistryService prompts *BusinessSystemPromptService @@ -30,7 +30,7 @@ func NewPromptDomainRuntime(registry *RemoteSkillRegistryService, prompts *Busin func promptPolicyAvailability(ctx context.Context) error { call, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - result, err := invokeProcessDomainExtension(call, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "prompt.availability", Payload: []byte(`{}`)}, true) + result, err := invokePromptSkills(call, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "prompt.availability", Payload: []byte(`{}`)}) if err != nil { return err } diff --git a/backend/internal/service/prompt_domain_runtime_test.go b/backend/internal/service/prompt_domain_runtime_test.go index b5e6107a71bc..8c89679c0a79 100644 --- a/backend/internal/service/prompt_domain_runtime_test.go +++ b/backend/internal/service/prompt_domain_runtime_test.go @@ -25,95 +25,6 @@ func (failedPromptProcess) InvokeOperation(context.Context, string, string, exte return extensionv1.Result{}, ErrExtensionOperationUnavailable } -type boundPromptFixture struct { - promptPolicyFixture - runtime *pluginRuntime -} - -func (f boundPromptFixture) BindOperationContext(ctx context.Context, _, _ string, _ extensionv1.Invocation) (context.Context, context.CancelFunc, error) { - return f.runtime.bindPolicyContext(ctx) -} - -type promptSourceFunc func(context.Context) (BusinessSystemPromptSourceCandidate, error) - -func (f promptSourceFunc) Fetch(ctx context.Context) (BusinessSystemPromptSourceCandidate, error) { - return f(ctx) -} - -func TestPromptSourceDisableDiscardsLateCandidateBeforePersistence(t *testing.T) { - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) - runtime := &pluginRuntime{} - processExtensionOperations.Store(&extensionOperationProvider{invoker: boundPromptFixture{runtime: runtime}}) - store := &fakeBusinessSystemPromptStore{} - prompts := NewBusinessSystemPromptService(store, nil) - prompts.SetBusinessSystemPromptSource(promptSourceFunc(func(ctx context.Context) (BusinessSystemPromptSourceCandidate, error) { - runtime.beginDrain() - return BusinessSystemPromptSourceCandidate{Body: "late"}, nil - })) - _, err := prompts.SyncManagedSource(context.Background(), 2, 42, 1, 3) - require.ErrorIs(t, err, context.Canceled) - require.Zero(t, store.sourceSyncCalls) -} - -func TestPromptDomainInitializesAfterActivationAndPreservesDataOnDisable(t *testing.T) { - previous := processExtensionOperations.Load() - t.Cleanup(func() { processExtensionOperations.Store(previous) }) - registry, registryStore, files := testRemoteSkillRegistry(t, testRemoteSkillCandidate(t, 1, 1, "seed")) - defer registry.Stop() - files.installed = false - store := &fakeBusinessSystemPromptStore{loaded: BusinessSystemPromptSnapshot{Revision: 3, Enabled: true, Body: "server"}} - prompts := NewBusinessSystemPromptService(store, nil) - defer prompts.Stop() - runtime := NewPromptDomainRuntime(registry, prompts) - processExtensionOperations.Store(nil) - runtime.reconcile(context.Background(), time.Now()) - require.False(t, files.installed) - require.False(t, store.seedCalled) - processExtensionOperations.Store(previous) - runtime.reconcile(context.Background(), time.Now()) - require.True(t, runtime.ready) - require.True(t, files.installed) - require.True(t, store.seedCalled) - _, err := registry.LoadPublishedFile(context.Background(), "SKILL.md") - require.NoError(t, err) - processExtensionOperations.Store(nil) - runtime.reconcile(context.Background(), time.Now()) - require.False(t, runtime.ready) - registry.runMu.Lock() - require.False(t, registry.started) - registry.runMu.Unlock() - prompts.lifecycleMu.Lock() - require.False(t, prompts.started) - prompts.lifecycleMu.Unlock() - require.Equal(t, "server", prompts.snapshot.Load().Body) - require.NotNil(t, registry.publication.Load()) - require.False(t, files.cleaned) - require.False(t, registryStore.cleaned) - _, err = registry.LoadPublishedFile(context.Background(), "SKILL.md") - require.ErrorIs(t, err, ErrRemoteSkillPublicFileNotFound) - body := []byte(`{"input":"client"}`) - gateway := &OpenAIGatewayService{businessPromptService: prompts} - out, applied, err := gateway.applyBusinessSystemPrompt(body, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, BusinessSystemPromptProtocolResponses, false) - require.NoError(t, err) - require.Equal(t, body, out) - require.False(t, applied.Applied) - processExtensionOperations.Store(previous) - runtime.reconcile(context.Background(), time.Now()) - require.True(t, runtime.ready) - registry.runMu.Lock() - require.True(t, registry.started) - registry.runMu.Unlock() - prompts.lifecycleMu.Lock() - require.True(t, prompts.started) - prompts.lifecycleMu.Unlock() - processExtensionOperations.Store(&extensionOperationProvider{invoker: failedPromptProcess{}}) - _, err = registry.LoadPublishedFile(context.Background(), "SKILL.md") - require.ErrorIs(t, err, ErrBusinessSystemPromptBundleUnavailable) - _, _, err = gateway.applyBusinessSystemPrompt(body, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, BusinessSystemPromptProtocolResponses, false) - require.ErrorIs(t, err, ErrBusinessSystemPromptUnavailable, "failure of an enabled policy must not silently remove a configured prompt") -} - func TestPromptDomainHealthyStartReloadsPromptOnce(t *testing.T) { previous := processExtensionOperations.Load() t.Cleanup(func() { processExtensionOperations.Store(previous) }) diff --git a/backend/internal/service/plugin_prompt_management.go b/backend/internal/service/prompt_management.go similarity index 89% rename from backend/internal/service/plugin_prompt_management.go rename to backend/internal/service/prompt_management.go index c9b1815a8681..249277b65288 100644 --- a/backend/internal/service/plugin_prompt_management.go +++ b/backend/internal/service/prompt_management.go @@ -14,10 +14,6 @@ import ( ) func invokePromptManagement(ctx context.Context, operation string, input, output any) error { - return invokePromptManagementPolicy(ctx, operation, input, output, false) -} - -func invokePromptManagementPolicy(ctx context.Context, operation string, input, output any, cached bool) error { raw, err := json.Marshal(input) if err != nil { return err @@ -27,7 +23,7 @@ func invokePromptManagementPolicy(ctx context.Context, operation string, input, if operation == "prompt.source.fetch" { call = ctx } - result, err := invokeProcessDomainExtension(call, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: operation, Payload: raw}, cached) + result, err := invokePromptSkills(call, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: operation, Payload: raw}) if err != nil { if strings.HasPrefix(operation, "prompt.source.") { return ErrBusinessSystemPromptSourceUnavailable @@ -100,7 +96,7 @@ func planSkillPublication(ctx context.Context, request extensionv1.SkillPublicat func LoadRemoteSkillRegistryProfile(ctx context.Context) (extensionv1.SkillRegistryPolicyProfile, error) { var profile extensionv1.SkillRegistryPolicyProfile - if err := invokePromptManagementPolicy(ctx, "skills.profile", struct{}{}, &profile, true); err != nil { + if err := invokePromptManagement(ctx, "skills.profile", struct{}{}, &profile); err != nil { return profile, err } if err := validateRemoteSkillRegistryProfile(profile); err != nil { @@ -136,10 +132,10 @@ func planRemoteSkillStorage(ctx context.Context, candidate RemoteSkillCandidate) return extensionv1.SkillStorageLayoutPlan{}, ErrBusinessSystemPromptBundleInvalid } var plan extensionv1.SkillStorageLayoutPlan - err := invokePromptManagementPolicy(ctx, "skills.storage.plan", extensionv1.SkillStorageLayoutRequest{ + err := invokePromptManagement(ctx, "skills.storage.plan", extensionv1.SkillStorageLayoutRequest{ BundleVersionID: candidate.Version.ID, PromptVersionID: candidate.Prompt.ID, EffectiveTreeSHA256: candidate.Version.EffectiveTreeSHA256, EffectivePromptSHA256: candidate.Prompt.EffectiveSHA256, - }, &plan, true) + }, &plan) if err != nil { return plan, err } @@ -166,7 +162,7 @@ func planRemoteSkillStorage(ctx context.Context, candidate RemoteSkillCandidate) func remoteSkillFilePlan(ctx context.Context, name string, data []byte) (extensionv1.SkillFilePlan, error) { var plan extensionv1.SkillFilePlan - err := invokePromptManagementPolicy(ctx, "skills.file.plan", extensionv1.SkillFileInspection{Path: name, Prefix: data[:min(2, len(data))], UTF8: utf8.Valid(data)}, &plan, true) + err := invokePromptManagement(ctx, "skills.file.plan", extensionv1.SkillFileInspection{Path: name, Prefix: data[:min(2, len(data))], UTF8: utf8.Valid(data)}, &plan) if err == nil && (plan.Kind != "text" && plan.Kind != "script" && plan.Kind != "binary" || len(plan.ReplaceFrom) > 2048 || len(plan.ReplaceTo) > 2048) { err = ErrBusinessSystemPromptBundleInvalid } diff --git a/backend/internal/service/prompt_skills_module.go b/backend/internal/service/prompt_skills_module.go new file mode 100644 index 000000000000..50cdbbb06fe7 --- /dev/null +++ b/backend/internal/service/prompt_skills_module.go @@ -0,0 +1,25 @@ +package service + +import ( + "context" + "sync" + + promptpolicy "github.com/Wei-Shaw/sub2api/internal/promptskills/policy" + extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" +) + +// promptSkills answers the prompt application and skill registry policy +// operations in process. The prompt-skills plugin served them before. +var promptSkills = sync.OnceValue(promptpolicy.New) + +// invokePromptSkills runs one prompt or skill registry operation. Tests replace +// it to model per-account decisions or restricted registry profiles. +var invokePromptSkills = func(ctx context.Context, in extensionv1.Invocation) (extensionv1.Result, error) { + return promptSkills().Invoke(ctx, in) +} + +// promptPlanInvoke adapts invokePromptSkills to the account-scoped invoker of +// the prompt application path. +func promptPlanInvoke(ctx context.Context, _, _ string, in extensionv1.Invocation) (extensionv1.Result, error) { + return invokePromptSkills(ctx, in) +} diff --git a/backend/internal/service/remote_skill_registry.go b/backend/internal/service/remote_skill_registry.go index 805897ff89be..d0f6984d8b13 100644 --- a/backend/internal/service/remote_skill_registry.go +++ b/backend/internal/service/remote_skill_registry.go @@ -355,13 +355,6 @@ func (s *RemoteSkillRegistryService) runSyncJob(ctx context.Context, job RemoteS s.failSyncJob(ctx, job.ID, "sync_expired") return } - bound, release, err := bindProcessDomainExtensionContext(ctx, extensionv1.Invocation{Capability: extensionv1.CapabilityRequest, Operation: "skills.tree.check"}) - if err != nil { - s.failSyncJob(ctx, job.ID, "service_stopped") - return - } - defer release() - ctx = bound if err := s.store.UpdateRemoteSkillSyncJobStage(ctx, job.ID, "fetching_source"); err != nil { s.failSyncJob(ctx, job.ID, "storage_error") return diff --git a/backend/internal/service/remote_skill_registry_filesystem.go b/backend/internal/service/remote_skill_registry_filesystem.go index 5bb9114b4cb5..ca32dba2268e 100644 --- a/backend/internal/service/remote_skill_registry_filesystem.go +++ b/backend/internal/service/remote_skill_registry_filesystem.go @@ -78,7 +78,7 @@ func (f *RemoteSkillRegistryFilesystem) LoadSeed(ctx context.Context) (RemoteSki return RemoteSkillCandidate{}, err } var promptBody string - if err := invokePromptManagementPolicy(ctx, "skills.prompt.seed", struct{}{}, &promptBody, true); err != nil { + if err := invokePromptManagement(ctx, "skills.prompt.seed", struct{}{}, &promptBody); err != nil { return RemoteSkillCandidate{}, err } prompt, err := buildRemoteSkillPromptCapture([]byte(promptBody)) diff --git a/backend/internal/service/remote_skill_registry_manifest.go b/backend/internal/service/remote_skill_registry_manifest.go index 23d4c0d60cc6..bf611a98a5ac 100644 --- a/backend/internal/service/remote_skill_registry_manifest.go +++ b/backend/internal/service/remote_skill_registry_manifest.go @@ -17,7 +17,7 @@ func loadRemoteSkillManifest() (remoteSkillManifest, error) { } func loadRemoteSkillManifestContext(ctx context.Context) (remoteSkillManifest, error) { var manifest remoteSkillManifest - err := invokePromptManagementPolicy(ctx, "skills.seed.index", struct{}{}, &manifest, true) + err := invokePromptManagement(ctx, "skills.seed.index", struct{}{}, &manifest) if err != nil { return manifest, err } @@ -67,7 +67,7 @@ func readRemoteSkillSeedFile(ctx context.Context, entry remoteSkillManifestEntry body := make([]byte, 0, entry.ByteLength) for len(body) < entry.ByteLength { var chunk extensionv1.SkillSeedChunk - err := invokePromptManagementPolicy(ctx, "skills.seed.chunk", extensionv1.SkillSeedChunkRequest{Path: entry.Path, Offset: len(body), Limit: 256 << 10}, &chunk, true) + err := invokePromptManagement(ctx, "skills.seed.chunk", extensionv1.SkillSeedChunkRequest{Path: entry.Path, Offset: len(body), Limit: 256 << 10}, &chunk) if err != nil { return nil, err } diff --git a/backend/internal/service/remote_skill_seed_fixture_test.go b/backend/internal/service/remote_skill_seed_fixture_test.go index 0b7acdb9035e..c9f50dce5bd9 100644 --- a/backend/internal/service/remote_skill_seed_fixture_test.go +++ b/backend/internal/service/remote_skill_seed_fixture_test.go @@ -6,8 +6,8 @@ import ( "io/fs" "strings" - registry "github.com/HTExplicit/sub2api-plugins/promptskills/registry" - promptsource "github.com/HTExplicit/sub2api-plugins/promptskills/source" + registry "github.com/Wei-Shaw/sub2api/internal/promptskills/registry" + promptsource "github.com/Wei-Shaw/sub2api/internal/promptskills/source" ) const ( diff --git a/backend/internal/testextensions/fixtures.go b/backend/internal/testextensions/fixtures.go index c610d02ddf77..ae82502d48b5 100644 --- a/backend/internal/testextensions/fixtures.go +++ b/backend/internal/testextensions/fixtures.go @@ -13,7 +13,7 @@ import ( codexrecovery "github.com/HTExplicit/sub2api-plugins/codexruntime/recovery" accounttools "github.com/HTExplicit/sub2api-plugins/accounttools/policy" - prompt "github.com/HTExplicit/sub2api-plugins/promptskills/policy" + prompt "github.com/Wei-Shaw/sub2api/internal/promptskills/policy" "github.com/Wei-Shaw/sub2api/internal/service" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) diff --git a/plugins/prompt-skills/ui/src/rules-api.ts b/frontend/src/api/admin/systemPromptRules.ts similarity index 64% rename from plugins/prompt-skills/ui/src/rules-api.ts rename to frontend/src/api/admin/systemPromptRules.ts index edf40e53f774..e0b2b4425294 100644 --- a/plugins/prompt-skills/ui/src/rules-api.ts +++ b/frontend/src/api/admin/systemPromptRules.ts @@ -1,4 +1,4 @@ -import { resource } from '@sub2api/plugin-ui' +import { apiClient } from '../client' export type PromptDelivery = 'native_control' | 'system' | 'developer' export type PromptPosition = 'control_prepend' | 'control_append' | 'conversation_head' | 'conversation_tail' @@ -36,10 +36,16 @@ export interface PromptPreview { } } export const legalPromptPosition = (delivery: PromptDelivery, position: PromptPosition) => delivery !== 'native_control' || position.startsWith('control_') +const basePath = '/admin/system-prompts' + export const rulesAPI = { - read: () => resource('prompts.rules.read'), - save: (policy: PromptRulePolicy, revision: number) => resource('prompts.rules.update', { body: { policy, expected_revision: revision } }), - accounts: (ids: number[]) => resource('prompts.bindings.read', { body: { account_ids: ids } }), - bind: (updates: Array<{ account_id: number; expected_updated_at: string; binding: PromptBinding }>, revision: number) => resource('prompts.bindings.update', { body: { updates, expected_revision: revision } }), - preview: (accountId: number, body: { protocol: string; transport: string; compact: boolean; body: unknown; policy?: PromptRulePolicy; binding?: PromptBinding; simulate_enabled?: boolean }) => resource('prompts.rules.preview', { params: { account_id: accountId }, body }) + read: async () => (await apiClient.get(`${basePath}/rules`)).data, + save: async (policy: PromptRulePolicy, revision: number) => + (await apiClient.put(`${basePath}/rules`, { policy, expected_revision: revision })).data, + accounts: async (ids: number[]) => + (await apiClient.post(`${basePath}/accounts/resolve`, { account_ids: ids })).data, + bind: async (updates: Array<{ account_id: number; expected_updated_at: string; binding: PromptBinding }>, revision: number) => + (await apiClient.post(`${basePath}/accounts/bindings`, { updates, expected_revision: revision })).data, + preview: async (accountId: number, body: { protocol: string; transport: string; compact: boolean; body: unknown; policy?: PromptRulePolicy; binding?: PromptBinding; simulate_enabled?: boolean }) => + (await apiClient.post(`${basePath}/rules/preview/${encodeURIComponent(String(accountId))}`, body)).data, } diff --git a/frontend/src/components/admin/account/AccountActionMenu.vue b/frontend/src/components/admin/account/AccountActionMenu.vue index f22e0c75a3aa..ba6b0c1c5fc1 100644 --- a/frontend/src/components/admin/account/AccountActionMenu.vue +++ b/frontend/src/components/admin/account/AccountActionMenu.vue @@ -13,6 +13,10 @@
diff --git a/frontend/src/components/admin/account/AccountPromptBindingPanel.vue b/frontend/src/components/admin/account/AccountPromptBindingPanel.vue index 081de0797263..85377610308c 100644 --- a/frontend/src/components/admin/account/AccountPromptBindingPanel.vue +++ b/frontend/src/components/admin/account/AccountPromptBindingPanel.vue @@ -1,10 +1,61 @@ diff --git a/plugins/prompt-skills/ui/src/PromptRulesManager.vue b/frontend/src/components/admin/systemPrompt/PromptRulesManager.vue similarity index 98% rename from plugins/prompt-skills/ui/src/PromptRulesManager.vue rename to frontend/src/components/admin/systemPrompt/PromptRulesManager.vue index 0a9d2fd6017c..031a5f02c5b8 100644 --- a/plugins/prompt-skills/ui/src/PromptRulesManager.vue +++ b/frontend/src/components/admin/systemPrompt/PromptRulesManager.vue @@ -82,12 +82,13 @@ diff --git a/plugins/prompt-skills/ui/src/__tests__/locales.spec.ts b/frontend/src/views/admin/__tests__/SystemPromptsView.locale.spec.ts similarity index 93% rename from plugins/prompt-skills/ui/src/__tests__/locales.spec.ts rename to frontend/src/views/admin/__tests__/SystemPromptsView.locale.spec.ts index 12b08c60d84e..095ca6a60cc8 100644 --- a/plugins/prompt-skills/ui/src/__tests__/locales.spec.ts +++ b/frontend/src/views/admin/__tests__/SystemPromptsView.locale.spec.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { createI18n } from 'vue-i18n' -import en from '../locales/en' -import zh from '../locales/zh' +import en from '@/i18n/locales/en/admin/systemPrompts' +import zh from '@/i18n/locales/zh/admin/systemPrompts' const api = vi.hoisted(() => ({ list: vi.fn(), get: vi.fn(), create: vi.fn(), saveDraft: vi.fn(), updateMetadata: vi.fn(), @@ -10,13 +10,10 @@ const api = vi.hoisted(() => ({ getSkillRegistry: vi.fn(), startSkillSync: vi.fn(), publishSkillVersion: vi.fn(), })) const notifications = vi.hoisted(() => ({ showError: vi.fn(), showSuccess: vi.fn(), showWarning: vi.fn() })) -vi.mock('../api', () => ({ default: api })) -vi.mock('@sub2api/plugin-ui', async () => ({ - ...await vi.importActual('@sub2api/plugin-ui'), - useNotifications: () => notifications, -})) +vi.mock('@/api/admin/systemPrompts', () => ({ default: api })) +vi.mock('@/stores/app', () => ({ useAppStore: () => notifications })) -import App from '../App.vue' +import App from '../SystemPromptsView.vue' function messages(value: object, prefix = ''): Record { return Object.fromEntries(Object.entries(value).flatMap(([key, child]) => { @@ -63,6 +60,7 @@ describe('prompt-skills site language', () => { zh: { admin: zh, common: { confirm: '确认', cancel: '取消', close: '关闭' } }, } }) const wrapper = mount(App, { global: { plugins: [i18n], stubs: { + AppLayout: { template: '
' }, Icon: true, Toggle: true, SystemPromptAdvancedDrawer: true, BaseDialog: true, ConfirmDialog: true, } } }) diff --git a/plugins/prompt-skills/ui/src/__tests__/App.spec.ts b/frontend/src/views/admin/__tests__/SystemPromptsView.spec.ts similarity index 98% rename from plugins/prompt-skills/ui/src/__tests__/App.spec.ts rename to frontend/src/views/admin/__tests__/SystemPromptsView.spec.ts index e2a2b0c2f508..06e6050d9d33 100644 --- a/plugins/prompt-skills/ui/src/__tests__/App.spec.ts +++ b/frontend/src/views/admin/__tests__/SystemPromptsView.spec.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent } from 'vue' import { flushPromises, mount } from '@vue/test-utils' -import SystemPromptsView from '../App.vue' -import SystemPromptAdvancedDrawer from '../SystemPromptAdvancedDrawer.vue' +import SystemPromptsView from '../SystemPromptsView.vue' +import SystemPromptAdvancedDrawer from '@/components/admin/systemPrompt/SystemPromptAdvancedDrawer.vue' const mocks = vi.hoisted(() => ({ list: vi.fn(), get: vi.fn(), create: vi.fn(), updateMetadata: vi.fn(), saveDraft: vi.fn(), @@ -11,13 +11,13 @@ const mocks = vi.hoisted(() => ({ showSuccess: vi.fn(), showError: vi.fn(), showWarning: vi.fn(), })) -vi.mock('../api', () => ({ default: mocks })) -vi.mock('@sub2api/plugin-ui', async () => ({ ...await vi.importActual('@sub2api/plugin-ui'), useNotifications: () => ({ +vi.mock('@/api/admin/systemPrompts', () => ({ default: mocks })) +vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showSuccess: mocks.showSuccess, showError: mocks.showError, showWarning: mocks.showWarning, }) })) vi.mock('vue-i18n', async () => { const actual = await vi.importActual('vue-i18n') - const { default: messages } = await import('../locales/en') + const { default: messages } = await import('@/i18n/locales/en/admin/systemPrompts') const en = { admin: messages, common: { confirm: 'Confirm', cancel: 'Cancel' } } const resolveMessage = (key: string) => key.split('.').reduce((value, segment) => { if (!value || typeof value !== 'object') return undefined diff --git a/frontend/src/views/admin/__tests__/systemPromptsIntegrationSurface.spec.ts b/frontend/src/views/admin/__tests__/systemPromptsIntegrationSurface.spec.ts index ba23c96b0bee..7bae7af10758 100644 --- a/frontend/src/views/admin/__tests__/systemPromptsIntegrationSurface.spec.ts +++ b/frontend/src/views/admin/__tests__/systemPromptsIntegrationSurface.spec.ts @@ -2,8 +2,6 @@ import { readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' -import en from '@/i18n/locales/en' -import zh from '@/i18n/locales/zh' const here = dirname(fileURLToPath(import.meta.url)) const read = (path: string) => readFileSync(resolve(here, path), 'utf8') @@ -24,14 +22,11 @@ describe('System Prompts integration surface', () => { expect(sidebar).toContain("t('nav.systemPrompts')") }) - it('keeps the page Chinese under the English locale and removes legacy surfaces', () => { - expect(en.admin.systemPrompts).toEqual(zh.admin.systemPrompts) + it('renders the native management page without legacy surfaces', () => { const view = read('../SystemPromptsView.vue') - expect(view).toContain('ExtensionPage') - expect(view).not.toContain('systemPromptsAPI') - const page = read('../../../../../plugins/prompt-skills/ui/src/App.vue') + expect(view).not.toContain('ExtensionPage') for (const marker of ['saveVersion', 'setCurrent', 'rollback', 'syncManagedSource', 'SystemPromptAdvancedDrawer']) { - expect(page).toContain(marker) + expect(view).toContain(marker) } expect(view).not.toContain('previewMerge') expect(view).not.toContain('previewUpstream') @@ -39,7 +34,7 @@ describe('System Prompts integration surface', () => { expect(view).not.toContain('isLegacyComposition') expect(view).not.toContain('copyInstallCommand') - const drawer = read('../../../../../plugins/prompt-skills/ui/src/SystemPromptAdvancedDrawer.vue') + const drawer = read('../../../components/admin/systemPrompt/SystemPromptAdvancedDrawer.vue') expect(drawer).toContain('compact_enabled') expect(drawer).toContain('expose_server_prompt') expect(drawer).toContain('data-test="system-prompt-advanced-drawer"') diff --git a/plugins/bundle.source.json b/plugins/bundle.source.json index 04ab5f0c4e9f..243042a77215 100644 --- a/plugins/bundle.source.json +++ b/plugins/bundle.source.json @@ -2,7 +2,6 @@ "schema_version": 1, "plugins": [ { "directory": "codex-runtime", "default_enabled": true, "migration": "codex-tickets-v1" }, - { "directory": "prompt-skills", "default_enabled": true }, { "directory": "account-tools", "default_enabled": true }, { "directory": "cindy-provider", "default_enabled": true, "migration": "cindy-provider-v1" } ] diff --git a/plugins/prompt-skills/cmd/plugin/main.go b/plugins/prompt-skills/cmd/plugin/main.go deleted file mode 100644 index 4af4fbf8bf74..000000000000 --- a/plugins/prompt-skills/cmd/plugin/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - policy "github.com/HTExplicit/sub2api-plugins/promptskills/policy" - extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" -) - -var version = "0.2.11" - -func main() { - extensionv1.Serve(extensionv1.Definition{ID: "codexrip.prompt-skills", Version: version, Capabilities: []string{extensionv1.CapabilityRequest, extensionv1.CapabilityAdmin}}, policy.New()) -} diff --git a/plugins/prompt-skills/go.mod b/plugins/prompt-skills/go.mod deleted file mode 100644 index d33a2dff34b7..000000000000 --- a/plugins/prompt-skills/go.mod +++ /dev/null @@ -1,30 +0,0 @@ -module github.com/HTExplicit/sub2api-plugins/promptskills - -go 1.27.0 - -require ( - github.com/Wei-Shaw/sub2api v0.0.0 - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - github.com/stretchr/testify v1.11.1 - golang.org/x/text v0.41.0 -) - -require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-plugin v1.8.0 // indirect - github.com/hashicorp/yamux v0.1.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/run v1.1.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/sys v0.47.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.2 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/Wei-Shaw/sub2api => ../../backend diff --git a/plugins/prompt-skills/go.sum b/plugins/prompt-skills/go.sum deleted file mode 100644 index 4b8c2d2a59da..000000000000 --- a/plugins/prompt-skills/go.sum +++ /dev/null @@ -1,84 +0,0 @@ -github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= -github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= -github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs= -github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= -github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= -github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= -github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/prompt-skills/manifest.source.json b/plugins/prompt-skills/manifest.source.json deleted file mode 100644 index 5d8cca67c609..000000000000 --- a/plugins/prompt-skills/manifest.source.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "schema_version": 1, - "id": "codexrip.prompt-skills", - "name": "提示词与技能策略", - "version": "0.2.13", - "description": "提示词完整性、应用范围与协议载体策略", - "author": "HTExplicit", - "requires": { "sub2api": ">=0.2.7-codexrip.2 <0.2.9", "tested_sub2api_versions": [], "plugin_protocol": 1, "transport_api": 1, "ui_bridge": 1, "extension_api": 1 }, - "capabilities": [ - { "id": "extensions.request.v1", "platform": "openai", "account_type": "*" }, - { "id": "extensions.admin.v1", "platform": "*", "account_type": "*" } - ], - "operations": { "extensions.request.v1": ["prompt.plan", "prompt.rules.validate", "prompt.availability", "prompt.composition", "prompt.template", "prompt.publication.plan", "prompt.seeds", "prompt.source.fetch", "prompt.source.validate", "skills.capture", "skills.prompt.seed", "skills.profile", "skills.seed.index", "skills.seed.chunk", "skills.manifest.validate", "skills.tree.check", "skills.file.plan", "skills.publication.plan", "skills.storage.plan"] }, - "resources": [ - { "name": "prompts.rules.read", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.rules.update", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.rules.preview", "capability": "extensions.admin.v1", "permission": "admin" }, - { "name": "prompts.bindings.read", "capability": "extensions.admin.v1", "permission": "admin" }, - { "name": "prompts.bindings.update", "capability": "extensions.admin.v1", "permission": "admin" }, - { "name": "prompts.list", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.create", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.runtime.read", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.runtime.update", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.preview.merge", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.preview.upstream", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.registry", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.versions", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.version", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.sync.start", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.sync.read", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.publish", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "skills.rollback", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.read", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.versions", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.update", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.delete", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.duplicate", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.draft", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.source.sync", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.publish", "capability": "extensions.request.v1", "permission": "admin" }, - { "name": "prompts.rollback", "capability": "extensions.request.v1", "permission": "admin" } - ], - "contributions": [ - { "id": "prompt-account-binding-action", "slot": "account.actions", "capability": "extensions.admin.v1", "entrypoint": "ui/compiled/index.html", "events": ["changed"], "permission": "admin", "account_filter": { "platforms": ["openai", "cindy"], "types": ["oauth", "setup-token", "apikey"] }, "label": { "zh": "账号提示词", "en": "Account prompts" } }, - { "id": "prompt-account-binding", "slot": "surface", "capability": "extensions.admin.v1", "entrypoint": "ui/compiled/index.html", "events": ["changed"], "permission": "admin", "label": { "zh": "账号提示词", "en": "Account prompts" } }, - { "id": "prompt-management", "slot": "surface", "capability": "extensions.request.v1", "entrypoint": "ui/compiled/index.html", "permission": "admin", "label": { "zh": "提示词与技能管理", "en": "Prompt and skill management" } }, - { "id": "prompt-policy", "slot": "admin.settings", "capability": "extensions.admin.v1", "action": "prompt.describe", "entrypoint": "ui/index.html", "permission": "admin", "label": { "zh": "提示词应用策略", "en": "Prompt application policy" } } - ], - "runtimes": {}, - "ui": { "entrypoint": "ui/index.html" }, - "files": {} -} diff --git a/plugins/prompt-skills/scripts/check-ui.py b/plugins/prompt-skills/scripts/check-ui.py deleted file mode 100644 index ae91dcd741ed..000000000000 --- a/plugins/prompt-skills/scripts/check-ui.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Offline acceptance of the compiled, sandboxed prompt management UI.""" -import argparse -import json -import mimetypes -import threading -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from urllib.parse import urlsplit - -from playwright.sync_api import expect, sync_playwright - -ROOT = Path(__file__).resolve().parents[3] -COMPILED = ROOT / "plugins/prompt-skills/ui/compiled" -HARNESS = r'''''' - - -class Handler(BaseHTTPRequestHandler): - def log_message(self, *_): - pass - - def do_GET(self): - requested = urlsplit(self.path).path - if requested == "/": - content, mime = HARNESS.encode(), "text/html; charset=utf-8" - else: - assets = {"/ui/" + file.relative_to(COMPILED).as_posix(): file for file in COMPILED.rglob("*") if file.is_file()} - if requested not in assets: - self.send_error(404) - return - content = assets[requested].read_bytes() - mime = mimetypes.guess_type(requested)[0] or "application/octet-stream" - self.send_response(200) - self.send_header("Content-Type", mime) - self.send_header("Cache-Control", "no-store") - if requested.startswith("/ui/"): - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Cross-Origin-Resource-Policy", "cross-origin") - self.send_header("Content-Security-Policy", "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'") - self.end_headers() - self.wfile.write(content) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--output", type=Path, required=True) - args = parser.parse_args() - assert (COMPILED / "index.html").is_file(), "Build the independent prompt UI first" - args.output.mkdir(parents=True, exist_ok=True) - server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) - threading.Thread(target=server.serve_forever, daemon=True).start() - cases = [] - try: - with sync_playwright() as playwright: - browser = playwright.chromium.launch() - try: - for width in (900, 390): - for theme in ("light", "dark"): - page = browser.new_page(viewport={"width": width, "height": 960}) - errors, external = [], [] - page.on("pageerror", lambda error: errors.append(str(error))) - def route_request(route): - if urlsplit(route.request.url).hostname != "127.0.0.1": - external.append(route.request.url) - route.abort() - else: - route.continue_() - page.route("**/*", route_request) - page.goto(f"http://127.0.0.1:{server.server_port}/?theme={theme}") - frame = page.frame_locator("iframe") - body = frame.locator('[data-test="system-prompt-body"]') - expect(body).to_have_value("初始草稿") - labels = frame.get_by_role("button").all_text_contents() - assert labels - body.fill("更新后的离线草稿") - frame.locator('[data-test="system-prompt-save-version"]').click() - page.wait_for_function("saved.length===1") - assert page.evaluate("saved[0].body") == "更新后的离线草稿" - assert page.evaluate("saved[0].expected_revision") == 5 - body.fill("故障期间保留的输入") - page.evaluate("setPluginAvailable(false)") - expect(frame.locator("#app")).to_have_attribute("inert", "") - expect(body).to_have_value("故障期间保留的输入") - page.evaluate("setPluginAvailable(true)") - expect(frame.locator("#app")).not_to_have_attribute("inert", "") - frame.locator('[data-test="system-prompt-open-advanced"]').click() - expect(frame.locator('[data-test="system-prompt-advanced-drawer"]')).to_be_visible() - page.screenshot(path=str(args.output / f"prompt-{width}-{theme}.png"), full_page=True) - assert frame.locator("body").evaluate("node=>node.scrollWidth<=innerWidth"), "horizontal overflow" - assert not errors, errors - assert not external, external - cases.append({"width": width, "theme": theme, "saved": True, "input_preserved": True, "page_errors": errors, "external_requests": len(external)}) - page.close() - finally: - browser.close() - finally: - server.shutdown() - server.server_close() - result = {"offline_only": True, "compiled_sandbox_ui": True, "cases": cases} - (args.output / "prompt-ui-check.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - print(json.dumps(result, ensure_ascii=False)) - - -if __name__ == "__main__": - main() diff --git a/plugins/prompt-skills/ui/assets/app.js b/plugins/prompt-skills/ui/assets/app.js deleted file mode 100644 index 8aa541570dcd..000000000000 --- a/plugins/prompt-skills/ui/assets/app.js +++ /dev/null @@ -1,19 +0,0 @@ -/* global Sub2APIPluginBridge */ -(async () => { - const bridge = new Sub2APIPluginBridge() - window.addEventListener('pagehide', () => bridge.dispose(), { once: true }) - const context = await bridge.context() - document.documentElement.dataset.theme = context.theme || 'light' - const en = String(context.locale).startsWith('en') - const text = (zh, english) => en ? english : zh - document.getElementById('title').textContent = text('提示词应用策略', 'Prompt application policy') - document.getElementById('description').textContent = text('使用当前已发布模板。应用前核对内容摘要与长度,并遵守模板的启用、压缩请求和可见性设置。', 'Uses the currently published template. Content integrity, activation, compaction and visibility settings are checked before application.') - const limits = await bridge.invoke('prompt.describe') - const list = document.getElementById('limits') - for (const [label, value] of [[text('模板上限', 'Template limit'), limits.document_limit], [text('组合提示词上限', 'Compiled prompt limit'), limits.compiled_limit]]) { - const term = document.createElement('dt'), definition = document.createElement('dd') - term.textContent = label; definition.textContent = `${(value / 1024).toLocaleString()} KiB` - list.append(term, definition) - } - bridge.resize(document.documentElement.scrollHeight) -})().catch(error => { document.getElementById('description').textContent = error.message || 'Policy unavailable' }) diff --git a/plugins/prompt-skills/ui/assets/styles.css b/plugins/prompt-skills/ui/assets/styles.css deleted file mode 100644 index 2a94eaf2c3ee..000000000000 --- a/plugins/prompt-skills/ui/assets/styles.css +++ /dev/null @@ -1,4 +0,0 @@ -:root{color-scheme:light;--bg:#fff;--fg:#1f2937;--muted:#6b7280;--line:#d1d5db} -:root[data-theme=dark]{color-scheme:dark;--bg:#111827;--fg:#e5e7eb;--muted:#9ca3af;--line:#374151} -body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.6 system-ui,sans-serif} -main{padding:20px;max-width:920px;margin:auto}h2{font-size:18px;margin:0 0 16px}p{color:var(--muted)}dt{font-weight:600;margin-top:16px}dd{margin:4px 0 0;padding-bottom:12px;border-bottom:1px solid var(--line)} diff --git a/plugins/prompt-skills/ui/index.html b/plugins/prompt-skills/ui/index.html deleted file mode 100644 index e9830448b987..000000000000 --- a/plugins/prompt-skills/ui/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -提示词策略 -

diff --git a/plugins/prompt-skills/ui/src/AccountPromptBinding.vue b/plugins/prompt-skills/ui/src/AccountPromptBinding.vue deleted file mode 100644 index ca71a57acd3d..000000000000 --- a/plugins/prompt-skills/ui/src/AccountPromptBinding.vue +++ /dev/null @@ -1,60 +0,0 @@ - - diff --git a/plugins/prompt-skills/ui/src/App.vue b/plugins/prompt-skills/ui/src/App.vue deleted file mode 100644 index d73a8bd7a021..000000000000 --- a/plugins/prompt-skills/ui/src/App.vue +++ /dev/null @@ -1,694 +0,0 @@ - - - diff --git a/plugins/prompt-skills/ui/src/api.ts b/plugins/prompt-skills/ui/src/api.ts deleted file mode 100644 index 3c7c7bfea630..000000000000 --- a/plugins/prompt-skills/ui/src/api.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { resource } from '@sub2api/plugin-ui' - -export type SystemPromptCompositionMode = 'inline' | 'codex_skill_hybrid' -export type SystemPromptClientMode = 'codex' | 'openai_compatible' -export type RemoteSkillSourceID = 'moxinggang' - -export interface SystemPromptRuntime { - enabled: boolean - expose_server_prompt: boolean - compact_enabled: boolean - template_id: number - version_id: number - template_version: number - revision: number - sha256: string - byte_length: number - degraded: boolean - composition_mode: SystemPromptCompositionMode - bundle_id: string - bundle_manifest_sha256: string - registry_revision?: number - registry_raw_tree_sha256?: string - registry_effective_tree_sha256?: string - registry_prompt_raw_sha256?: string - registry_prompt_effective_sha256?: string - registry_upstream_source_id?: RemoteSkillSourceID - registry_upstream_root?: string - registry_public_root?: string - bundle_available: boolean - bundle_degraded: boolean - degraded_reason?: string - updated_at: string -} - -export interface SystemPromptTemplate { - id: number - slug: string - name: string - description: string - is_seed: boolean - managed_source?: string - created_by?: number - updated_by?: number - created_at: string - updated_at: string -} - -export interface SystemPromptVersion { - id: number - template_id: number - version: number - body: string - sha256: string - byte_length: number - note: string - composition_mode: SystemPromptCompositionMode - bundle_id: string - bundle_manifest_sha256: string - created_by?: number - published_at?: string - published_by?: number - created_at: string - is_active: boolean - source_repository?: string - source_commit?: string - source_version?: string - source_artifact?: string - source_artifact_sha256?: string - source_license_sha256?: string -} - -export interface RemoteSkillBundleVersion { - id: number - upstream_source_id: RemoteSkillSourceID - upstream_root: string - public_root: string - raw_tree_sha256: string - effective_tree_sha256: string - prompt_version_id: number - file_count: number - raw_total_bytes: number - effective_total_bytes: number - added_files: number - modified_files: number - deleted_files: number - script_changes: number - binary_changes: number - fetched_at: string - created_by?: number - published_at?: string - published_by?: number - created_at: string -} - -export interface RemoteSkillPromptVersion { - id: number - raw_sha256: string - effective_sha256: string - diff: string - created_by?: number - created_at: string -} - -export interface RemoteSkillPromptVersionDetail extends RemoteSkillPromptVersion { - raw_body: string - effective_body: string -} - -export interface RemoteSkillFileChange { - path: string - change: 'added' | 'modified' | 'deleted' - kind: string - raw_sha256?: string - effective_sha256?: string - previous_effective_sha256?: string -} - -export interface RemoteSkillBundleVersionDetail extends RemoteSkillBundleVersion { - prompt: RemoteSkillPromptVersionDetail - file_changes: RemoteSkillFileChange[] - verified: boolean -} - -export interface RemoteSkillRegistrySnapshot { - revision: number - active?: RemoteSkillBundleVersion - active_prompt?: RemoteSkillPromptVersion - degraded: boolean - degraded_reason?: string - updated_at: string -} - -export interface RemoteSkillRegistryResponse { - runtime: RemoteSkillRegistrySnapshot - versions: RemoteSkillBundleVersion[] - source: { - upstream_source_id: RemoteSkillSourceID - upstream_root: string - public_root: string - } -} - -export interface RemoteSkillSyncJob { - id: number - status: 'queued' | 'running' | 'succeeded' | 'failed' - progress_stage: string - candidate_bundle_version_id?: number - prompt_capture_provided: boolean - error_code?: string - created_at: string - started_at?: string - completed_at?: string -} - -export type ManagedSourceSyncStatus = 'up_to_date' | 'no_prompt_change' | 'candidate_created' - -export interface ManagedSourceSyncVersion { - id: number - template_id: number - version: number - sha256: string - byte_length: number - source_repository?: string - source_commit?: string - source_version?: string - source_artifact?: string - source_artifact_sha256?: string - source_license_sha256?: string -} - -export interface ManagedSourceSyncResponse { - status: ManagedSourceSyncStatus - version?: ManagedSourceSyncVersion -} - -export interface SystemPromptListResponse { - templates: SystemPromptTemplate[] - runtime: SystemPromptRuntime -} - -export interface SystemPromptDetailResponse { - template: SystemPromptTemplate - versions: SystemPromptVersion[] - runtime: SystemPromptRuntime -} - -export interface CreateSystemPromptRequest { - slug: string - name: string - description: string - body: string - note: string - composition_mode?: SystemPromptCompositionMode - bundle_id?: string - bundle_manifest_sha256?: string - expected_revision: number -} - -export interface PreviewUpstreamResponse { - body: unknown - client_mode: SystemPromptClientMode - base_server_instructions: string - final_server_instructions: string - application: { - applied: boolean - carrier: string - client_instructions: string - server_instructions: string - revision: number - sha256: string - base_sha256?: string - effective_sha256?: string - effective_byte_length?: number - bundle_id?: string - bundle_manifest_sha256?: string - bundle_revision?: number - bundle_raw_tree_sha256?: string - bundle_effective_tree_sha256?: string - bundle_prompt_raw_sha256?: string - bundle_prompt_effective_sha256?: string - bundle_upstream_source_id?: string - bundle_upstream_root?: string - bundle_public_root?: string - degraded?: boolean - degraded_reason?: string - } -} - - -export const list = () => resource('prompts.list') -export const get = (id: number) => resource('prompts.read', { params: { id } }) -export const listVersions = (id: number) => resource('prompts.versions', { params: { id } }) -export const getSkillRegistry = () => resource('skills.registry') -export const getSkillVersion = (id: number) => resource('skills.version', { params: { bundle_version_id: id } }) -export const getSkillSync = (id: number) => resource('skills.sync.read', { params: { sync_id: id } }) - -export function startSkillSync(expectedRevision: number, promptCapture?: File) { - const form: Array<[string, string | Blob]> = [['expected_revision', String(expectedRevision)]] - if (promptCapture) form.push(['prompt_capture', promptCapture]) - return resource('skills.sync.start', { form }) -} - -export function publishSkillVersion(id: number, expectedRevision: number, rollback = false) { - return resource(rollback ? 'skills.rollback' : 'skills.publish', { - params: { bundle_version_id: id }, body: { expected_revision: expectedRevision } - }) -} - -export const create = (body: CreateSystemPromptRequest) => resource('prompts.create', { body }) -export function updateMetadata(id: number, body: { name?: string; description?: string; expected_revision: number }) { - return resource('prompts.update', { params: { id }, body }) -} - -export function saveDraft(id: number, body: { - body: string; note: string; composition_mode: SystemPromptCompositionMode; bundle_id: string - bundle_manifest_sha256: string; expected_latest_version: number; expected_revision: number -}) { - return resource('prompts.draft', { params: { id }, body }) -} - -export function syncManagedSource(id: number, body: { expected_latest_version: number; expected_revision: number }) { - return resource('prompts.source.sync', { params: { id }, body }) -} - -export function publish(id: number, versionId: number, expectedRevision: number, rollback = false) { - return resource(rollback ? 'prompts.rollback' : 'prompts.publish', { - params: { id, version_id: versionId }, body: { expected_revision: expectedRevision } - }) -} - -export function updateRuntime(body: { expected_revision: number; enabled: boolean; expose_server_prompt: boolean; compact_enabled: boolean }) { - return resource('prompts.runtime.update', { body }) -} - -export function duplicate(id: number, body: { slug: string; name: string; expected_revision: number }) { - return resource('prompts.duplicate', { params: { id }, body }) -} - -export function remove(id: number, expectedRevision: number) { - return resource<{ deleted: boolean }>('prompts.delete', { params: { id }, query: { expected_revision: expectedRevision } }) -} - -export function previewMerge(body: { - template_id?: number; version_id?: number; client_instructions: string; server_instructions?: string - composition_mode?: SystemPromptCompositionMode; bundle_id?: string; bundle_manifest_sha256?: string - body?: unknown; client_mode: SystemPromptClientMode -}) { - return resource<{ instructions: string; client_mode: SystemPromptClientMode; base_server_instructions: string; final_server_instructions: string; application: PreviewUpstreamResponse['application'] }>('prompts.preview.merge', { body }) -} - -export function previewUpstream(body: { - template_id: number; version_id: number; server_instructions?: string; composition_mode?: SystemPromptCompositionMode - bundle_id?: string; bundle_manifest_sha256?: string; protocol: 'responses' | 'chat'; compact: boolean - body: unknown; client_mode: SystemPromptClientMode -}) { - return resource('prompts.preview.upstream', { body }) -} - -export default { list, get, listVersions, getSkillRegistry, getSkillVersion, startSkillSync, getSkillSync, - publishSkillVersion, create, updateMetadata, saveDraft, syncManagedSource, publish, updateRuntime, - duplicate, remove, previewMerge, previewUpstream } diff --git a/plugins/prompt-skills/ui/src/index.html b/plugins/prompt-skills/ui/src/index.html deleted file mode 100644 index 1c108c4b3e56..000000000000 --- a/plugins/prompt-skills/ui/src/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -提示词与技能管理
diff --git a/plugins/prompt-skills/ui/src/main.ts b/plugins/prompt-skills/ui/src/main.ts deleted file mode 100644 index bcf902935dde..000000000000 --- a/plugins/prompt-skills/ui/src/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mountPlugin } from '@sub2api/plugin-ui' -import '@sub2api/plugin-ui/base.css' -import App from './App.vue' -import zh from './locales/zh' -import en from './locales/en' - -void mountPlugin(App, { - zh: { admin: zh, common: { confirm: '确认', cancel: '取消', close: '关闭', loading: '加载中', error: '操作失败' } }, - en: { admin: en, common: { confirm: 'Confirm', cancel: 'Cancel', close: 'Close', loading: 'Loading', error: 'Operation failed' } } -}).catch(error => { document.getElementById('app')!.textContent = error instanceof Error ? error.message : 'Plugin unavailable' }) From c38df363beb21bb5f1c34661a6f4284687faf765 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 12:10:25 +0800 Subject: [PATCH 05/13] fix(deplugin): resolve the official model catalog without the plugin manager model-policy was the only plugin that declared a model catalog, and it now resolves in process. The capacity lookup still asked the plugin manager first, so an unavailable plugin registry (for example a bundle that fails version admission) would also hide the built-in official catalog. The lookup now answers from the official catalog alone; the unused match merge helper and the host-lookup rollout test for plugin catalogs are removed. Co-Authored-By: Claude Opus 5.5 --- .../service/model_context_capacity_catalog.go | 18 ++------ .../service/model_context_official_catalog.go | 20 --------- .../model_context_official_catalog_test.go | 18 -------- .../internal/service/plugin_catalog_test.go | 42 ------------------- 4 files changed, 4 insertions(+), 94 deletions(-) diff --git a/backend/internal/service/model_context_capacity_catalog.go b/backend/internal/service/model_context_capacity_catalog.go index 4ab23df41277..6862bf70d24a 100644 --- a/backend/internal/service/model_context_capacity_catalog.go +++ b/backend/internal/service/model_context_capacity_catalog.go @@ -8,32 +8,22 @@ import ( "sort" "strconv" "sync/atomic" - "time" ) type extensionCatalogProvider struct{ resolver extensionv1.CatalogResolver } var processExtensionCatalog atomic.Pointer[extensionCatalogProvider] +// lookupExtensionCatalog resolves the release-pinned official catalog. No +// remaining plugin declares a model catalog, so plugin health cannot hide it. func lookupExtensionCatalog(query extensionv1.CatalogQuery) *OfficialModelContextCapacity { official, err := resolveOfficialModelCatalog(query) if err != nil { return nil } - provider := processExtensionCatalog.Load() - if provider == nil { - return official.Entry - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - // Remaining plugin catalogs (Cindy) still answer through the plugin manager. - // An unavailable enabled catalog keeps the lookup conservative, as before. - result, err := provider.resolver.ResolveCatalog(ctx, query) - if err != nil { - return nil - } - return mergeCatalogMatches(official, result).Entry + return official.Entry } + func (m *PluginManager) ResolveCatalog(ctx context.Context, query extensionv1.CatalogQuery) (extensionv1.CatalogMatch, error) { if err := ctx.Err(); err != nil { return extensionv1.CatalogMatch{}, err diff --git a/backend/internal/service/model_context_official_catalog.go b/backend/internal/service/model_context_official_catalog.go index 85c832f1cc41..6f66d822907b 100644 --- a/backend/internal/service/model_context_official_catalog.go +++ b/backend/internal/service/model_context_official_catalog.go @@ -106,23 +106,3 @@ func officialCatalogApplies(query extensionv1.CatalogQuery, entry OfficialModelC } return true } - -// mergeCatalogMatches combines ordered catalog answers with the plugin -// manager's rule: an ambiguous answer, or two different capacities, is -// ambiguous; otherwise the later equal-capacity entry wins. -func mergeCatalogMatches(matches ...extensionv1.CatalogMatch) extensionv1.CatalogMatch { - var resolved extensionv1.CatalogMatch - for _, match := range matches { - if !match.Matched { - continue - } - if match.Entry == nil { - return match - } - if resolved.Entry != nil && resolved.Entry.ModelContextCapacity != match.Entry.ModelContextCapacity { - return extensionv1.CatalogMatch{Matched: true} - } - resolved = match - } - return resolved -} diff --git a/backend/internal/service/model_context_official_catalog_test.go b/backend/internal/service/model_context_official_catalog_test.go index 24766456bb04..c3ae2200e5fa 100644 --- a/backend/internal/service/model_context_official_catalog_test.go +++ b/backend/internal/service/model_context_official_catalog_test.go @@ -57,21 +57,3 @@ func TestOfficialModelCatalogGPT6SeparatesAPIAndCodex(t *testing.T) { } } } - -func TestMergeCatalogMatchesKeepsPluginAmbiguityRule(t *testing.T) { - a := &OfficialModelContextCapacity{ModelID: "a", ModelContextCapacity: ModelContextCapacity{ContextWindow: 100}} - b := &OfficialModelContextCapacity{ModelID: "b", ModelContextCapacity: ModelContextCapacity{ContextWindow: 200}} - same := &OfficialModelContextCapacity{ModelID: "same", ModelContextCapacity: ModelContextCapacity{ContextWindow: 100}} - if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true, Entry: a}, extensionv1.CatalogMatch{}); got.Entry != a { - t.Fatalf("a single match must win: %+v", got) - } - if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true, Entry: a}, extensionv1.CatalogMatch{Matched: true, Entry: b}); !got.Matched || got.Entry != nil { - t.Fatalf("different capacities must be ambiguous: %+v", got) - } - if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true, Entry: a}, extensionv1.CatalogMatch{Matched: true, Entry: same}); got.Entry != same { - t.Fatalf("the later equal-capacity entry must win: %+v", got) - } - if got := mergeCatalogMatches(extensionv1.CatalogMatch{Matched: true}, extensionv1.CatalogMatch{Matched: true, Entry: a}); !got.Matched || got.Entry != nil { - t.Fatalf("an ambiguous answer must stay ambiguous: %+v", got) - } -} diff --git a/backend/internal/service/plugin_catalog_test.go b/backend/internal/service/plugin_catalog_test.go index 12ebfba1a142..3c3acdbf0258 100644 --- a/backend/internal/service/plugin_catalog_test.go +++ b/backend/internal/service/plugin_catalog_test.go @@ -114,48 +114,6 @@ func TestPluginCatalogCacheRejectsConfiguringAndCanceledRequests(t *testing.T) { require.Equal(t, 1, calls, "neither rejected request may reach the plugin") } -func TestPluginCatalogCarriesAccountIDThroughRolloutAndCache(t *testing.T) { - var calls []int64 - manager, _ := catalogGuardTestManager(t, func(in extensionv1.Invocation) (extensionv1.Result, error) { - var query extensionv1.CatalogQuery - require.NoError(t, json.Unmarshal(in.Payload, &query)) - require.Equal(t, in.AccountID, query.AccountID, "the invocation gate and cache query must use the same host account") - calls = append(calls, in.AccountID) - raw, err := json.Marshal(extensionv1.CatalogMatch{Matched: true, Entry: &OfficialModelContextCapacity{ModelContextCapacity: ModelContextCapacity{ContextWindow: 200000 + in.AccountID}}}) - return extensionv1.Result{Payload: raw}, err - }) - previous := processExtensionCatalog.Load() - t.Cleanup(func() { processExtensionCatalog.Store(previous) }) - processExtensionCatalog.Store(&extensionCatalogProvider{resolver: manager}) - var included, excluded int64 - for id := int64(1); included == 0 || excluded == 0; id++ { - if stablePluginBucket(id) < 50 { - included = id - } else { - excluded = id - } - } - account := &Account{ID: included, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"base_url": "https://api.openai.com/v1"}} - installation := manager.extensions.Load().installations[1] - installation.Bindings[0].RolloutPercent = 0 - require.Nil(t, LookupOfficialModelContextCapacity(account, "test-model")) - require.Empty(t, calls, "0%% rollout must not invoke a catalog for a real account") - installation.Bindings[0].RolloutPercent = 50 - first := LookupOfficialModelContextCapacity(account, "test-model") - require.NotNil(t, first) - require.EqualValues(t, 200000+included, first.ContextWindow) - account.ID = excluded - require.Nil(t, LookupOfficialModelContextCapacity(account, "test-model")) - require.Equal(t, []int64{included}, calls) - installation.Bindings[0].RolloutPercent = 100 - second := LookupOfficialModelContextCapacity(account, "test-model") - require.NotNil(t, second) - require.EqualValues(t, 200000+excluded, second.ContextWindow, "another account cannot reuse the first account's cache") - account.ID = included - require.EqualValues(t, 200000+included, LookupOfficialModelContextCapacity(account, "test-model").ContextWindow) - require.Equal(t, []int64{included, excluded}, calls, "the original account should reuse only its own cache") -} - func TestPluginCatalogCacheChecksPersistedScopeGenerationAndConfig(t *testing.T) { calls := 0 manager, repo := catalogGuardTestManager(t, func(extensionv1.Invocation) (extensionv1.Result, error) { From 4d7b7903d6be838b96257bd39c074c8e4c51d824 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 12:19:32 +0800 Subject: [PATCH 06/13] fix(deplugin): start Image Studio when it is switched on after boot The server started the Image Studio runtime only when the feature was on at boot, so enabling it later from the settings panel queued jobs that never ran until a restart. The server now registers the runtime start with the image tools switches, and switching Image Studio on starts it (a running runtime is left alone). Workers still refuse claims while the feature is off. Co-Authored-By: Claude Opus 5.5 --- backend/cmd/server/main.go | 8 ++++++ backend/internal/service/image_tools.go | 25 +++++++++++++++---- .../service/image_tools_policy_test.go | 18 +++++++++++++ .../internal/service/setting_image_tools.go | 4 +-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 84b0339112c6..0cef71ccc0cd 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -235,6 +235,14 @@ func runMainServer() { os.Exit(1) } } + if app.ImageStudio != nil { + studio := app.ImageStudio + service.SetImageStudioStarter(func() { + if err := studio.Start(context.Background()); err != nil { + log.Printf("Image Studio could not start: %v", err) + } + }) + } if app.ImageStudio != nil && service.ImageStudioFeatureEnabled() { if err := app.ImageStudio.Start(context.Background()); err != nil { log.Printf("Image Studio could not start: %v", err) diff --git a/backend/internal/service/image_tools.go b/backend/internal/service/image_tools.go index 3e0dd51dc4e1..734436751ab9 100644 --- a/backend/internal/service/image_tools.go +++ b/backend/internal/service/image_tools.go @@ -30,16 +30,31 @@ var imageStudioStop = struct { ch chan struct{} }{ch: make(chan struct{})} +// imageStudioStarter starts the Image Studio background runtime. The server +// registers it so that switching Image Studio on after boot needs no restart. +var imageStudioStarter atomic.Pointer[func()] + +// SetImageStudioStarter registers the runtime start used when Image Studio is +// switched on after boot. Starting an already running runtime is a no-op. +func SetImageStudioStarter(start func()) { + imageStudioStarter.Store(&start) +} + // ConfigureImageTools installs the effective switches (startup load, admin // update, tests). A nil value falls back to the deploy-time rollout flags. func ConfigureImageTools(config *extensionv1.ImageToolsConfig) { imageToolsConfigOverride.Store(config) - if current, _ := currentImageToolsConfig(); !current.StudioEnabled { - imageStudioStop.mu.Lock() - close(imageStudioStop.ch) - imageStudioStop.ch = make(chan struct{}) - imageStudioStop.mu.Unlock() + current, _ := currentImageToolsConfig() + if current.StudioEnabled { + if start := imageStudioStarter.Load(); start != nil { + (*start)() + } + return } + imageStudioStop.mu.Lock() + close(imageStudioStop.ch) + imageStudioStop.ch = make(chan struct{}) + imageStudioStop.mu.Unlock() } // bindImageStudioEnabled returns a context that is canceled when Image Studio diff --git a/backend/internal/service/image_tools_policy_test.go b/backend/internal/service/image_tools_policy_test.go index 6f01be13e516..93dea1690030 100644 --- a/backend/internal/service/image_tools_policy_test.go +++ b/backend/internal/service/image_tools_policy_test.go @@ -124,3 +124,21 @@ func TestImageToolsStudioPlanRequiresExplicitEnablementAndKeepsNativeOne(t *test t.Fatalf("supported editing plan unavailable: %+v %v", plan, err) } } + +func TestSwitchingImageStudioOnStartsTheRuntime(t *testing.T) { + previousConfig, previousStarter := imageToolsConfigOverride.Load(), imageStudioStarter.Load() + t.Cleanup(func() { + imageToolsConfigOverride.Store(previousConfig) + imageStudioStarter.Store(previousStarter) + }) + starts := 0 + SetImageStudioStarter(func() { starts++ }) + ConfigureImageTools(&extensionv1.ImageToolsConfig{}) + if starts != 0 { + t.Fatalf("switching Image Studio off must not start the runtime, starts=%d", starts) + } + ConfigureImageTools(&extensionv1.ImageToolsConfig{StudioEnabled: true}) + if starts != 1 { + t.Fatalf("switching Image Studio on must start the runtime once, starts=%d", starts) + } +} diff --git a/backend/internal/service/setting_image_tools.go b/backend/internal/service/setting_image_tools.go index ac51c2c21086..8de43361efeb 100644 --- a/backend/internal/service/setting_image_tools.go +++ b/backend/internal/service/setting_image_tools.go @@ -28,8 +28,8 @@ func (s *SettingService) LoadImageToolsConfig(ctx context.Context) { } } -// UpdateImageToolsConfig persists the switches and applies them to this process. -// Image Studio's background runtime still starts only at boot, as before. +// UpdateImageToolsConfig persists the switches and applies them to this process, +// starting the Image Studio runtime when it is switched on. func (s *SettingService) UpdateImageToolsConfig(ctx context.Context, config extensionv1.ImageToolsConfig) error { if err := s.writeJSONSetting(ctx, SettingKeyImageToolsConfig, config); err != nil { return err From 1471b3cbcb7be8ef7f01849008067a510d4a42b4 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 12:54:11 +0800 Subject: [PATCH 07/13] test(deplugin): restore the Cindy provider process contract test Removing the prompt-skills process test in b821bdbfa also cut the adjacent Cindy provider test. cindy-provider is still a bundled plugin and the plugin CI script still selects this test by name, and an unmatched -run pattern passes silently, so the coverage would have disappeared unnoticed. Co-Authored-By: Claude Opus 5.5 --- .../service/plugin_extension_runtime_test.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/backend/internal/service/plugin_extension_runtime_test.go b/backend/internal/service/plugin_extension_runtime_test.go index e6a26f159eba..c72174b2a5b4 100644 --- a/backend/internal/service/plugin_extension_runtime_test.go +++ b/backend/internal/service/plugin_extension_runtime_test.go @@ -68,6 +68,36 @@ func TestExtensionRuntimeUsesOfficialProcessAndHostBroker(t *testing.T) { // HTTP upstream exists in this test, so it cannot issue a real model request. } +func TestCindyProviderExtensionRuntimeUsesIndependentProcess(t *testing.T) { + binary := os.Getenv("SUB2API_CINDY_PROVIDER_TEST_BINARY") + if binary == "" { + t.Skip("set SUB2API_CINDY_PROVIDER_TEST_BINARY to the independent Cindy provider program") + } + data, err := os.ReadFile(binary) + require.NoError(t, err) + digest := sha256.Sum256(data) + installation := &PluginInstallation{ID: 11, PluginKey: "codexrip.cindy-provider", Version: firstPartyTestVersion(t, "cindy-provider"), BinaryPath: binary, BinarySHA256: hex.EncodeToString(digest[:]), Manifest: PluginManifest{Requires: PluginRequirements{ExtensionAPI: 1}, Capabilities: []PluginCapability{{ID: extensionv1.CapabilityProvider, Platform: PlatformCindy, AccountType: AccountTypeAPIKey}}}} + host := newPluginHostServiceServer(installation.PluginKey, nil, nil, PluginAccountScope{}) + host.extension = &pluginExtensionHost{key: installation.PluginKey, state: &extensionReadOnlyFixture{}} + socketDir := filepath.Join(t.TempDir(), "runtime") + require.NoError(t, os.MkdirAll(socketDir, 0700)) + runtime, err := startPluginRuntime(context.Background(), installation, 15*time.Second, socketDir, host) + require.NoError(t, err) + t.Cleanup(runtime.kill) + require.NoError(t, runtime.validateAndApplyConfig(context.Background(), []byte(`{"catalog_enabled":true,"search_enabled":true}`))) + out, err := runtime.extension.Invoke(context.Background(), extensionv1.Invocation{Capability: extensionv1.CapabilityProvider, Operation: "cindy.pricing", Payload: []byte(`{}`)}) + require.NoError(t, err) + require.Empty(t, out.Code) + var snapshot extensionv1.CindyPricingSnapshot + require.NoError(t, json.Unmarshal(out.Payload, &snapshot)) + require.True(t, snapshot.Config.CatalogEnabled) + var pricing CindyTextPricing + var known bool + require.True(t, queryCindyPricingSnapshot(&snapshot, "CindyTextPricingForModel", "gpt-5.6-luna", []any{&pricing, &known})) + require.True(t, known) + require.Positive(t, pricing.InputCostPerToken) +} + func TestAccountToolsExtensionRuntimeUsesIndependentProcess(t *testing.T) { binary := os.Getenv("SUB2API_ACCOUNT_TOOLS_TEST_BINARY") if binary == "" { From f8bf2f638014bb179f5dc3434425f109cd0976c3 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 13:10:16 +0800 Subject: [PATCH 08/13] fix(deplugin): address review findings for image tools and observability - Switching Image Studio off now also cancels in-flight upstream image IO. That IO detaches from request cancellation and only honours policy signals, so the studio switch is registered as one, as the plugin policy lease was; before, the upstream kept running and was billed while the result could no longer be saved. - Saving the image tools or observability switches invalidates the served HTML public settings, and both panels refresh the public settings, so the Image Studio entry and the flat theme follow the saved value on reload. - The two settings endpoints reject unknown keys and non-boolean values like the former plugin validators, and their GET returns the switches this process applies. A failed settings read is logged instead of silently looking like an unset key. - Prompt Audit keeps upstream's rounded-* classes; the flat theme already zeroes the radius, so only the dark-mode contrast fixes stay different. Co-Authored-By: Claude Opus 5.5 --- .../internal/handler/admin/setting_handler.go | 23 ++++++----- .../service/admin_observability_test.go | 9 +++++ backend/internal/service/image_tools.go | 14 ++++++- .../service/image_tools_policy_test.go | 17 ++++++++ .../service/setting_admin_observability.go | 29 +++++++------- .../internal/service/setting_image_tools.go | 19 ++++----- backend/internal/service/setting_json.go | 39 +++++++++++++++++-- .../admin/ImageToolsSettingsPanel.vue | 4 ++ .../admin/ObservabilitySettingsPanel.vue | 3 ++ .../prompt-audit/components/EndpointPool.vue | 4 +- .../components/EventWorkspace.vue | 2 +- .../components/FilterDeleteDialog.vue | 2 +- .../prompt-audit/components/PolicyPanel.vue | 6 +-- .../components/RuntimeOverview.vue | 8 ++-- 14 files changed, 132 insertions(+), 47 deletions(-) diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 1d42096550f0..e0b9ae79660e 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -110,17 +110,20 @@ func (h *SettingHandler) GetOfficialModelContextCatalog(c *gin.Context) { }) } -// GetImageToolsSettings returns the Image Studio and Responses image bridge switches. +// GetImageToolsSettings returns the Image Studio and Responses image bridge +// switches this process applies. // GET /api/v1/admin/settings/image-tools func (h *SettingHandler) GetImageToolsSettings(c *gin.Context) { - response.Success(c, h.settingService.GetImageToolsConfig(c.Request.Context())) + response.Success(c, service.EffectiveImageToolsConfig()) } -// UpdateImageToolsSettings saves the Image Studio and Responses image bridge switches. +// UpdateImageToolsSettings saves the Image Studio and Responses image bridge +// switches. Omitted switches are off, as in the former plugin configuration. // PUT /api/v1/admin/settings/image-tools func (h *SettingHandler) UpdateImageToolsSettings(c *gin.Context) { var req extensionv1.ImageToolsConfig - if err := c.ShouldBindJSON(&req); err != nil { + raw, err := c.GetRawData() + if err != nil || service.DecodeSwitchSettings(raw, &req, "studio_enabled", "responses_image_enabled") != nil { response.BadRequest(c, "Invalid image tools settings") return } @@ -131,18 +134,20 @@ func (h *SettingHandler) UpdateImageToolsSettings(c *gin.Context) { response.Success(c, req) } -// GetObservabilitySettings returns the account traffic telemetry and flat theme switches. +// GetObservabilitySettings returns the account traffic telemetry and flat theme +// switches this process applies. // GET /api/v1/admin/settings/observability func (h *SettingHandler) GetObservabilitySettings(c *gin.Context) { - response.Success(c, h.settingService.GetAdminObservabilityConfig(c.Request.Context())) + response.Success(c, service.EffectiveAdminObservabilityConfig()) } -// UpdateObservabilitySettings saves the account traffic telemetry and flat theme switches. -// Omitted switches stay on, as they did in the former plugin configuration. +// UpdateObservabilitySettings saves the account traffic telemetry and flat theme +// switches. Omitted switches stay on, as in the former plugin configuration. // PUT /api/v1/admin/settings/observability func (h *SettingHandler) UpdateObservabilitySettings(c *gin.Context) { req := extensionv1.AdminObservabilityConfig{TelemetryEnabled: true, ThemeEnabled: true} - if err := c.ShouldBindJSON(&req); err != nil { + raw, err := c.GetRawData() + if err != nil || service.DecodeSwitchSettings(raw, &req, "telemetry_enabled", "theme_enabled") != nil { response.BadRequest(c, "Invalid observability settings") return } diff --git a/backend/internal/service/admin_observability_test.go b/backend/internal/service/admin_observability_test.go index 9a3c7eefbab8..7e4896f32706 100644 --- a/backend/internal/service/admin_observability_test.go +++ b/backend/internal/service/admin_observability_test.go @@ -114,3 +114,12 @@ func TestAccountTrafficOutcomeRulesKeepCancellationAboveErrorsAndRequireWSTermin require.Equal(t, string(tc.want), got) } } + +func TestDecodeSwitchSettingsRejectsUnknownAndNonBooleanValues(t *testing.T) { + config := extensionv1.AdminObservabilityConfig{TelemetryEnabled: true, ThemeEnabled: true} + require.NoError(t, DecodeSwitchSettings([]byte(`{"theme_enabled":false}`), &config, "telemetry_enabled", "theme_enabled")) + require.Equal(t, extensionv1.AdminObservabilityConfig{TelemetryEnabled: true}, config, "omitted switches keep their defaults") + for _, raw := range []string{`{"theme_enabled":null}`, `{"theme":true}`, `{"theme_enabled":"false"}`, `[]`, `null`} { + require.Error(t, DecodeSwitchSettings([]byte(raw), &config, "telemetry_enabled", "theme_enabled"), raw) + } +} diff --git a/backend/internal/service/image_tools.go b/backend/internal/service/image_tools.go index 734436751ab9..8e053704c0a3 100644 --- a/backend/internal/service/image_tools.go +++ b/backend/internal/service/image_tools.go @@ -58,20 +58,30 @@ func ConfigureImageTools(config *extensionv1.ImageToolsConfig) { } // bindImageStudioEnabled returns a context that is canceled when Image Studio -// is switched off. +// is switched off. The switch is also registered as a policy signal: upstream +// image IO detaches from request cancellation (detachUpstreamContext) but still +// honours policy signals, so switching off stops an in-flight generation +// instead of billing a result that can no longer be saved. func bindImageStudioEnabled(ctx context.Context) (context.Context, context.CancelFunc) { imageStudioStop.mu.Lock() stop := imageStudioStop.ch imageStudioStop.mu.Unlock() + signal, stopSignal := context.WithCancel(context.Background()) + signals, _ := ctx.Value(pluginPolicySignalsKey{}).([]context.Context) + ctx = context.WithValue(ctx, pluginPolicySignalsKey{}, append(append([]context.Context{}, signals...), signal)) ctx, cancel := context.WithCancel(ctx) go func() { select { case <-stop: + stopSignal() cancel() case <-ctx.Done(): } }() - return ctx, cancel + return ctx, func() { + stopSignal() + cancel() + } } // observeNativeImageFacts lets tests inspect the facts the host derives for the diff --git a/backend/internal/service/image_tools_policy_test.go b/backend/internal/service/image_tools_policy_test.go index 93dea1690030..b0ec3f5df50d 100644 --- a/backend/internal/service/image_tools_policy_test.go +++ b/backend/internal/service/image_tools_policy_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" extensionv1 "github.com/Wei-Shaw/sub2api/pkg/extensionapi/v1" ) @@ -142,3 +143,19 @@ func TestSwitchingImageStudioOnStartsTheRuntime(t *testing.T) { t.Fatalf("switching Image Studio on must start the runtime once, starts=%d", starts) } } + +func TestSwitchingImageStudioOffStopsDetachedUpstreamIO(t *testing.T) { + previous := imageToolsConfigOverride.Load() + t.Cleanup(func() { imageToolsConfigOverride.Store(previous) }) + ConfigureImageTools(&extensionv1.ImageToolsConfig{StudioEnabled: true}) + bound, release := bindImageStudioEnabled(context.Background()) + defer release() + upstream, releaseUpstream := detachUpstreamContext(bound) + defer releaseUpstream() + ConfigureImageTools(&extensionv1.ImageToolsConfig{}) + select { + case <-upstream.Done(): + case <-time.After(5 * time.Second): + t.Fatal("switching Image Studio off must cancel detached upstream IO") + } +} diff --git a/backend/internal/service/setting_admin_observability.go b/backend/internal/service/setting_admin_observability.go index 119c1740f3a7..773c7d4520e6 100644 --- a/backend/internal/service/setting_admin_observability.go +++ b/backend/internal/service/setting_admin_observability.go @@ -11,23 +11,22 @@ import ( // configuration. const SettingKeyAdminObservabilityConfig = "admin_observability_config" -// GetAdminObservabilityConfig returns the stored switches, or the deploy-time -// default when the administrator has not saved them yet. -func (s *SettingService) GetAdminObservabilityConfig(ctx context.Context) extensionv1.AdminObservabilityConfig { - stored := extensionv1.AdminObservabilityConfig{TelemetryEnabled: true, ThemeEnabled: true} - if s.readJSONSetting(ctx, SettingKeyAdminObservabilityConfig, &stored) { - return stored - } - if s == nil { - return LegacyAdminObservabilityConfig(nil) - } - return LegacyAdminObservabilityConfig(s.cfg) +// EffectiveAdminObservabilityConfig returns the switches this process applies. +func EffectiveAdminObservabilityConfig() extensionv1.AdminObservabilityConfig { + return currentAdminObservabilityConfig() } // LoadAdminObservabilityConfig installs the effective switches for this process -// at startup, including the deploy-time default. +// at startup: the stored value, or the deploy-time default when none is stored. func (s *SettingService) LoadAdminObservabilityConfig(ctx context.Context) { - config := s.GetAdminObservabilityConfig(ctx) + config := extensionv1.AdminObservabilityConfig{TelemetryEnabled: true, ThemeEnabled: true} + if !s.readJSONSetting(ctx, SettingKeyAdminObservabilityConfig, &config) { + if s == nil { + config = LegacyAdminObservabilityConfig(nil) + } else { + config = LegacyAdminObservabilityConfig(s.cfg) + } + } ConfigureAdminObservability(&config) } @@ -38,5 +37,9 @@ func (s *SettingService) UpdateAdminObservabilityConfig(ctx context.Context, con return err } ConfigureAdminObservability(&config) + // flat_theme_enabled is a public setting embedded in the served HTML. + if s.onUpdate != nil { + s.onUpdate() + } return nil } diff --git a/backend/internal/service/setting_image_tools.go b/backend/internal/service/setting_image_tools.go index 8de43361efeb..ae063cd7a711 100644 --- a/backend/internal/service/setting_image_tools.go +++ b/backend/internal/service/setting_image_tools.go @@ -10,17 +10,14 @@ import ( // switches formerly kept in the image-tools plugin configuration. const SettingKeyImageToolsConfig = "image_tools_config" -// GetImageToolsConfig returns the stored switches, or the deploy-time rollout -// flags when the administrator has not saved them yet. -func (s *SettingService) GetImageToolsConfig(ctx context.Context) extensionv1.ImageToolsConfig { - var stored extensionv1.ImageToolsConfig - if s.readJSONSetting(ctx, SettingKeyImageToolsConfig, &stored) { - return stored - } - return LegacyImageToolsConfig() +// EffectiveImageToolsConfig returns the switches this process applies. +func EffectiveImageToolsConfig() extensionv1.ImageToolsConfig { + config, _ := currentImageToolsConfig() + return config } -// LoadImageToolsConfig installs the stored switches for this process at startup. +// LoadImageToolsConfig installs the stored switches for this process at +// startup. Without a stored value the deploy-time rollout flags stay in force. func (s *SettingService) LoadImageToolsConfig(ctx context.Context) { var stored extensionv1.ImageToolsConfig if s.readJSONSetting(ctx, SettingKeyImageToolsConfig, &stored) { @@ -35,5 +32,9 @@ func (s *SettingService) UpdateImageToolsConfig(ctx context.Context, config exte return err } ConfigureImageTools(&config) + // image_studio_enabled is a public setting embedded in the served HTML. + if s.onUpdate != nil { + s.onUpdate() + } return nil } diff --git a/backend/internal/service/setting_json.go b/backend/internal/service/setting_json.go index 50e53243b98d..c75c51473aa0 100644 --- a/backend/internal/service/setting_json.go +++ b/backend/internal/service/setting_json.go @@ -4,11 +4,14 @@ import ( "context" "encoding/json" "errors" + "log/slog" "strings" ) // readJSONSetting decodes a JSON-valued setting into target. It reports false -// when the setting is unset, unreadable or not valid JSON for target. +// when the setting is unset, unreadable or not valid JSON for target; read +// failures other than an unset key are logged, since callers then fall back +// to defaults. func (s *SettingService) readJSONSetting(ctx context.Context, key string, target any) bool { if s == nil || s.settingRepo == nil { return false @@ -16,10 +19,20 @@ func (s *SettingService) readJSONSetting(ctx context.Context, key string, target dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout) defer cancel() raw, err := s.settingRepo.GetValue(dbCtx, key) - if err != nil || strings.TrimSpace(raw) == "" { + if err != nil { + if !errors.Is(err, ErrSettingNotFound) { + slog.Warn("json_setting_read_failed", "key", key, "error", err) + } + return false + } + if strings.TrimSpace(raw) == "" { + return false + } + if err := json.Unmarshal([]byte(raw), target); err != nil { + slog.Warn("json_setting_invalid", "key", key, "error", err) return false } - return json.Unmarshal([]byte(raw), target) == nil + return true } // writeJSONSetting stores value as a JSON-valued setting. @@ -35,3 +48,23 @@ func (s *SettingService) writeJSONSetting(ctx context.Context, key string, value defer cancel() return s.settingRepo.Set(dbCtx, key, string(raw)) } + +// DecodeSwitchSettings decodes a JSON object of boolean switches into target. +// Unknown keys and non-boolean values are rejected, as the former plugin +// configuration validators did; omitted switches keep target's values. +func DecodeSwitchSettings(raw []byte, target any, allowed ...string) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil || fields == nil { + return errors.New("settings must be a JSON object") + } + for key, value := range fields { + known := false + for _, name := range allowed { + known = known || key == name + } + if !known || (string(value) != "true" && string(value) != "false") { + return errors.New("unknown or invalid switch setting") + } + } + return json.Unmarshal(raw, target) +} diff --git a/frontend/src/components/admin/ImageToolsSettingsPanel.vue b/frontend/src/components/admin/ImageToolsSettingsPanel.vue index 7e3d42af626c..14244a9b481e 100644 --- a/frontend/src/components/admin/ImageToolsSettingsPanel.vue +++ b/frontend/src/components/admin/ImageToolsSettingsPanel.vue @@ -30,8 +30,10 @@ import { onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { getImageToolsSettings, updateImageToolsSettings, type ImageToolsSettings } from '@/api/admin/settings' +import { useAppStore } from '@/stores' const { t } = useI18n() +const appStore = useAppStore() const form = ref(null) const loadError = ref(false) const saving = ref(false) @@ -44,6 +46,8 @@ async function save() { try { form.value = await updateImageToolsSettings({ ...form.value }) status.value = t('admin.settings.imageTools.saved') + // The sidebar and route guard follow the public image_studio_enabled flag. + void appStore.fetchPublicSettings(true) } catch { status.value = t('admin.settings.imageTools.saveFailed') } finally { diff --git a/frontend/src/components/admin/ObservabilitySettingsPanel.vue b/frontend/src/components/admin/ObservabilitySettingsPanel.vue index 8d9e4ff2b247..c67a01b1287a 100644 --- a/frontend/src/components/admin/ObservabilitySettingsPanel.vue +++ b/frontend/src/components/admin/ObservabilitySettingsPanel.vue @@ -30,9 +30,11 @@ import { onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { getObservabilitySettings, updateObservabilitySettings, type ObservabilitySettings } from '@/api/admin/settings' +import { useAppStore } from '@/stores' import { applyFlatTheme } from '@/utils/flatTheme' const { t } = useI18n() +const appStore = useAppStore() const form = ref(null) const loadError = ref(false) const saving = ref(false) @@ -46,6 +48,7 @@ async function save() { form.value = await updateObservabilitySettings({ ...form.value }) applyFlatTheme(form.value.theme_enabled) status.value = t('admin.settings.observability.saved') + void appStore.fetchPublicSettings(true) } catch { status.value = t('admin.settings.observability.saveFailed') } finally { diff --git a/frontend/src/features/prompt-audit/components/EndpointPool.vue b/frontend/src/features/prompt-audit/components/EndpointPool.vue index e67ff74c80c2..041d93576416 100644 --- a/frontend/src/features/prompt-audit/components/EndpointPool.vue +++ b/frontend/src/features/prompt-audit/components/EndpointPool.vue @@ -10,10 +10,10 @@

-
+
{{ t('admin.promptAudit.pool.empty') }}
-
+
-
+
diff --git a/frontend/src/features/prompt-audit/components/FilterDeleteDialog.vue b/frontend/src/features/prompt-audit/components/FilterDeleteDialog.vue index 5da4bc664d94..555e5278cb16 100644 --- a/frontend/src/features/prompt-audit/components/FilterDeleteDialog.vue +++ b/frontend/src/features/prompt-audit/components/FilterDeleteDialog.vue @@ -53,7 +53,7 @@ -
+
{{ t('admin.promptAudit.events.moreConditions') }}
-
+
{{ t('admin.promptAudit.policy.scope') }}
@@ -53,7 +53,7 @@
-
+
-
+

{{ t('admin.promptAudit.policy.strategy') }}

priority · {{ t('admin.promptAudit.policy.strategyHint') }}

diff --git a/frontend/src/features/prompt-audit/components/RuntimeOverview.vue b/frontend/src/features/prompt-audit/components/RuntimeOverview.vue index 769203eb95fd..b85c2f330bfa 100644 --- a/frontend/src/features/prompt-audit/components/RuntimeOverview.vue +++ b/frontend/src/features/prompt-audit/components/RuntimeOverview.vue @@ -22,7 +22,7 @@