From 731ad8beeb1a1fbd073e8059e3a63d1ecfc487f9 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Mon, 1 Jun 2026 02:31:56 -0400 Subject: [PATCH 1/2] fix: bridge admin contribution grants --- internal/assets_test.go | 1 + internal/module_dashboard.go | 51 ++++++++++++++++++++--- internal/module_dashboard_test.go | 14 ++++--- internal/plugin.go | 19 ++++++--- internal/plugin_contracts_test.go | 28 +++++++++---- internal/registry.go | 11 +++++ internal/step_admin.go | 31 ++++++++++++-- internal/step_admin_test.go | 67 ++++++++++++++++++++++++++----- internal/ui_dist/index.html | 30 ++++++++++---- plugin.contracts.json | 5 +-- 10 files changed, 210 insertions(+), 47 deletions(-) diff --git a/internal/assets_test.go b/internal/assets_test.go index 31af3e6..0c39b82 100644 --- a/internal/assets_test.go +++ b/internal/assets_test.go @@ -19,6 +19,7 @@ func TestEmbeddedAdminShell(t *testing.T) { `workflow.admin.auth.request`, `workflow.admin.auth.response`, `grantedScopes: adminToolGrants`, + `payload.granted_permissions`, `adminToolFrames`, `event.origin !== window.location.origin`, `startsWith('/admin')`, diff --git a/internal/module_dashboard.go b/internal/module_dashboard.go index ca58070..cfbd413 100644 --- a/internal/module_dashboard.go +++ b/internal/module_dashboard.go @@ -78,8 +78,21 @@ func (m *dashboardModule) InvokeMethod(method string, args map[string]any) (map[ }, nil case "ListContributions": appContext, _ := args["app_context"].(string) - contributions := m.registry.listForPermissions(appContext, stringSliceValue(args, "granted_permissions")) - out := make([]map[string]any, 0, len(contributions)) + grantedPermissions := stringSliceValue(args, "granted_permissions") + contributions := contributionListValue(args["contributions"]) + if len(contributions) == 0 { + contributions = contributionListValue([]any{args["auth_contribution"], args["authz_contribution"]}) + } + if len(contributions) == 0 { + contributions = m.registry.listForPermissions(appContext, grantedPermissions) + } else { + grants := make(map[string]struct{}, len(grantedPermissions)) + for _, permission := range grantedPermissions { + grants[permission] = struct{}{} + } + contributions = filterContributionList(contributions, appContext, grants) + } + out := make([]any, 0, len(contributions)) for _, contribution := range contributions { out = append(out, contributionToMap(contribution)) } @@ -139,6 +152,26 @@ func contributionFromMap(args map[string]any) *contracts.AdminContribution { return contribution } +func contributionListValue(value any) []*contracts.AdminContribution { + switch items := value.(type) { + case []*contracts.AdminContribution: + return items + case []any: + out := make([]*contracts.AdminContribution, 0, len(items)) + for _, item := range items { + switch contribution := item.(type) { + case *contracts.AdminContribution: + out = append(out, contribution) + case map[string]any: + out = append(out, contributionFromMap(contribution)) + } + } + return out + default: + return nil + } +} + func contributionToMap(contribution *contracts.AdminContribution) map[string]any { if contribution == nil { return nil @@ -150,12 +183,20 @@ func contributionToMap(contribution *contracts.AdminContribution) map[string]any "path": contribution.Path, "render_mode": contribution.RenderMode, "app_context": contribution.AppContext, - "actions": append([]string(nil), contribution.Actions...), + "actions": stringAnySlice(contribution.Actions), "permissions": permissionMaps(contribution.Permissions), "metadata": contributionMetadataMap(contribution), } } +func stringAnySlice(values []string) []any { + out := make([]any, 0, len(values)) + for _, value := range values { + out = append(out, value) + } + return out +} + func contributionMetadataMap(contribution *contracts.AdminContribution) map[string]any { if contribution == nil || contribution.Metadata == nil { return map[string]any{} @@ -191,8 +232,8 @@ func permissionValues(value any) []*contracts.AdminPermission { } } -func permissionMaps(permissions []*contracts.AdminPermission) []map[string]any { - out := make([]map[string]any, 0, len(permissions)) +func permissionMaps(permissions []*contracts.AdminPermission) []any { + out := make([]any, 0, len(permissions)) for _, permission := range permissions { out = append(out, map[string]any{ "permission": permission.GetPermission(), diff --git a/internal/module_dashboard_test.go b/internal/module_dashboard_test.go index 1de3877..d0ba18c 100644 --- a/internal/module_dashboard_test.go +++ b/internal/module_dashboard_test.go @@ -114,16 +114,20 @@ func TestDashboardModuleServiceInvoker(t *testing.T) { if err != nil { t.Fatalf("ListContributions: %v", err) } - contributions, ok := listed["contributions"].([]map[string]any) + contributions, ok := listed["contributions"].([]any) if !ok { - t.Fatalf("contributions type = %T, want []map[string]any", listed["contributions"]) + t.Fatalf("contributions type = %T, want []any", listed["contributions"]) } - if len(contributions) != 1 || contributions[0]["id"] != "orders" { + first, ok := contributions[0].(map[string]any) + if !ok { + t.Fatalf("first contribution type = %T, want map[string]any", contributions[0]) + } + if len(contributions) != 1 || first["id"] != "orders" { t.Fatalf("unexpected contributions: %#v", contributions) } - metadata, ok := contributions[0]["metadata"].(map[string]any) + metadata, ok := first["metadata"].(map[string]any) if !ok { - t.Fatalf("metadata type = %T, want map[string]any", contributions[0]["metadata"]) + t.Fatalf("metadata type = %T, want map[string]any", first["metadata"]) } if metadata["validate_path"] != "/api/admin/orders/config/validate" { t.Fatalf("metadata validate_path = %v", metadata["validate_path"]) diff --git a/internal/plugin.go b/internal/plugin.go index 2bd7bc2..623b8cb 100644 --- a/internal/plugin.go +++ b/internal/plugin.go @@ -121,7 +121,10 @@ func (p *adminPlugin) StepTypes() []string { } func (p *adminPlugin) TypedStepTypes() []string { - return p.StepTypes() + return []string{ + "step.admin_authorize_action", + "step.admin_resource_action", + } } func (p *adminPlugin) CreateStep(typeName, _ string, config map[string]any) (sdk.StepInstance, error) { @@ -142,9 +145,9 @@ func (p *adminPlugin) CreateStep(typeName, _ string, config map[string]any) (sdk func (p *adminPlugin) CreateTypedStep(typeName, name string, config *anypb.Any) (sdk.StepInstance, error) { switch typeName { case "step.admin_register_contribution": - return sdk.NewTypedStepFactory(typeName, &contracts.AdminStepConfig{}, &contracts.RegisterContributionInput{}, typedRegisterContribution).CreateTypedStep(typeName, name, config) + return nil, fmt.Errorf("%w: step type %q", sdk.ErrTypedContractNotHandled, typeName) case "step.admin_list_contributions": - return sdk.NewTypedStepFactory(typeName, &contracts.AdminStepConfig{}, &contracts.ListContributionsInput{}, typedListContributions).CreateTypedStep(typeName, name, config) + return nil, fmt.Errorf("%w: step type %q", sdk.ErrTypedContractNotHandled, typeName) case "step.admin_authorize_action": return sdk.NewTypedStepFactory(typeName, &contracts.AdminStepConfig{}, &contracts.AuthorizeAdminActionInput{}, typedAuthorizeAction).CreateTypedStep(typeName, name, config) case "step.admin_resource_action": @@ -163,8 +166,8 @@ func (p *adminPlugin) ContractRegistry() *pb.ContractRegistry { }}, Contracts: []*pb.ContractDescriptor{ adminModuleContract("admin.dashboard", "AdminDashboardConfig"), - adminStepContract("step.admin_register_contribution", "AdminStepConfig", "RegisterContributionInput", "RegisterContributionOutput"), - adminStepContract("step.admin_list_contributions", "AdminStepConfig", "ListContributionsInput", "ListContributionsOutput"), + adminStepContractWithMode("step.admin_register_contribution", "AdminStepConfig", "RegisterContributionInput", "RegisterContributionOutput", pb.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT), + adminStepContractWithMode("step.admin_list_contributions", "AdminStepConfig", "ListContributionsInput", "ListContributionsOutput", pb.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT), adminStepContract("step.admin_authorize_action", "AdminStepConfig", "AuthorizeAdminActionInput", "AuthorizeAdminActionOutput"), adminStepContract("step.admin_resource_action", "AdminStepConfig", "AdminResourceActionInput", "AdminResourceActionOutput"), adminServiceContract("admin.dashboard", "AdminDashboard", "RegisterContribution", "RegisterContributionInput", "RegisterContributionOutput"), @@ -186,6 +189,10 @@ func adminModuleContract(moduleType, configMessage string) *pb.ContractDescripto } func adminStepContract(stepType, configMessage, inputMessage, outputMessage string) *pb.ContractDescriptor { + return adminStepContractWithMode(stepType, configMessage, inputMessage, outputMessage, pb.ContractMode_CONTRACT_MODE_STRICT_PROTO) +} + +func adminStepContractWithMode(stepType, configMessage, inputMessage, outputMessage string, mode pb.ContractMode) *pb.ContractDescriptor { const pkg = "workflow.plugins.admin.v1." return &pb.ContractDescriptor{ Kind: pb.ContractKind_CONTRACT_KIND_STEP, @@ -193,7 +200,7 @@ func adminStepContract(stepType, configMessage, inputMessage, outputMessage stri ConfigMessage: pkg + configMessage, InputMessage: pkg + inputMessage, OutputMessage: pkg + outputMessage, - Mode: pb.ContractMode_CONTRACT_MODE_STRICT_PROTO, + Mode: mode, } } diff --git a/internal/plugin_contracts_test.go b/internal/plugin_contracts_test.go index 0f56946..21a6438 100644 --- a/internal/plugin_contracts_test.go +++ b/internal/plugin_contracts_test.go @@ -27,7 +27,11 @@ func TestAdminPlugin_ContractRegistry(t *testing.T) { contracts := make(map[string]*proto.ContractDescriptor, len(registry.Contracts)) for _, contract := range registry.Contracts { - if contract.Mode != proto.ContractMode_CONTRACT_MODE_STRICT_PROTO { + if contractKey(contract) == "step:step.admin_register_contribution" || contractKey(contract) == "step:step.admin_list_contributions" { + if contract.Mode != proto.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT { + t.Fatalf("%s mode = %s, want proto with legacy struct", contractKey(contract), contract.Mode) + } + } else if contract.Mode != proto.ContractMode_CONTRACT_MODE_STRICT_PROTO { t.Fatalf("%s mode = %s, want strict proto", contractKey(contract), contract.Mode) } key := contractKey(contract) @@ -60,8 +64,8 @@ func TestAdminPlugin_PluginContractsJSON(t *testing.T) { if !ok { t.Fatalf("%s missing from runtime contracts", key) } - if manifest.Config != runtimeContract.ConfigMessage || manifest.Input != runtimeContract.InputMessage || manifest.Output != runtimeContract.OutputMessage { - t.Fatalf("%s manifest = %#v, runtime config/input/output = %q/%q/%q", key, manifest, runtimeContract.ConfigMessage, runtimeContract.InputMessage, runtimeContract.OutputMessage) + if manifest.Config != runtimeContract.ConfigMessage || manifest.Input != runtimeContract.InputMessage || manifest.Output != runtimeContract.OutputMessage || manifest.Mode != contractModeString(runtimeContract.Mode) { + t.Fatalf("%s manifest = %#v, runtime mode/config/input/output = %q/%q/%q/%q", key, manifest, contractModeString(runtimeContract.Mode), runtimeContract.ConfigMessage, runtimeContract.InputMessage, runtimeContract.OutputMessage) } } } @@ -102,6 +106,20 @@ type pluginContract struct { Config string `json:"config"` Input string `json:"input"` Output string `json:"output"` + Mode string `json:"mode"` +} + +func contractModeString(mode proto.ContractMode) string { + switch mode { + case proto.ContractMode_CONTRACT_MODE_STRICT_PROTO: + return "strict" + case proto.ContractMode_CONTRACT_MODE_PROTO_WITH_LEGACY_STRUCT: + return "proto_with_legacy_struct" + case proto.ContractMode_CONTRACT_MODE_LEGACY_STRUCT: + return "legacy_struct" + default: + return "" + } } func readPluginContracts(t *testing.T) map[string]pluginContract { @@ -121,7 +139,6 @@ func readPluginContracts(t *testing.T) map[string]pluginContract { Type string `json:"type"` ServiceName string `json:"serviceName"` Method string `json:"method"` - Mode string `json:"mode"` pluginContract } `json:"contracts"` } @@ -133,9 +150,6 @@ func readPluginContracts(t *testing.T) map[string]pluginContract { } out := make(map[string]pluginContract, len(manifest.Contracts)) for _, contract := range manifest.Contracts { - if contract.Mode != "strict" { - t.Fatalf("%s mode = %q, want strict", contract.Type, contract.Mode) - } var key string switch contract.Kind { case "module": diff --git a/internal/registry.go b/internal/registry.go index 5a9747a..5c95199 100644 --- a/internal/registry.go +++ b/internal/registry.go @@ -60,6 +60,17 @@ func (r *contributionRegistry) listForPermissions(appContext string, grantedPerm out := make([]*contracts.AdminContribution, 0, len(r.contributions)) for _, contribution := range r.contributions { + out = append(out, proto.Clone(contribution).(*contracts.AdminContribution)) + } + return filterContributionList(out, appContext, grants) +} + +func filterContributionList(contributions []*contracts.AdminContribution, appContext string, grants map[string]struct{}) []*contracts.AdminContribution { + out := make([]*contracts.AdminContribution, 0, len(contributions)) + for _, contribution := range contributions { + if contribution == nil { + continue + } if appContext != "" && contribution.AppContext != "" && contribution.AppContext != appContext { continue } diff --git a/internal/step_admin.go b/internal/step_admin.go index 2a1cf12..4ca8d89 100644 --- a/internal/step_admin.go +++ b/internal/step_admin.go @@ -22,11 +22,22 @@ func newAdminStep(method string, config map[string]any) *adminStep { return &adminStep{method: method, module: module} } -func (s *adminStep) Execute(_ context.Context, _ map[string]any, _ map[string]map[string]any, current map[string]any, _ map[string]any, config map[string]any) (*sdk.StepResult, error) { +func (s *adminStep) Execute(_ context.Context, _ map[string]any, stepOutputs map[string]map[string]any, current map[string]any, _ map[string]any, config map[string]any) (*sdk.StepResult, error) { args := mergeMaps(config, current) if args["module"] == nil { args["module"] = s.module } + if s.method == "ListContributions" && args["contributions"] == nil { + contributions := make([]any, 0) + for _, stepName := range stringSliceValue(args, "contribution_steps") { + if contribution := stepOutputs[stepName]["contribution"]; contribution != nil { + contributions = append(contributions, contribution) + } + } + if len(contributions) > 0 { + args["contributions"] = contributions + } + } moduleName := stringValue(args, "module") module, err := lookupDashboardModule(moduleName) if err != nil { @@ -58,11 +69,15 @@ func typedRegisterContribution(ctx context.Context, req sdk.TypedStepRequest[*co if err != nil { return nil, err } - if err := module.registry.register(req.Input.GetContribution()); err != nil { + contribution := req.Input.GetContribution() + if contribution == nil { + contribution = contributionFromMap(req.Current) + } + if err := module.registry.register(contribution); err != nil { return &sdk.TypedStepResult[*contracts.RegisterContributionOutput]{Output: &contracts.RegisterContributionOutput{Error: err.Error()}}, nil } return &sdk.TypedStepResult[*contracts.RegisterContributionOutput]{ - Output: &contracts.RegisterContributionOutput{Registered: true, Contribution: req.Input.GetContribution()}, + Output: &contracts.RegisterContributionOutput{Registered: true, Contribution: contribution}, }, nil } @@ -75,8 +90,16 @@ func typedListContributions(_ context.Context, req sdk.TypedStepRequest[*contrac if err != nil { return nil, err } + appContext := req.Input.GetAppContext() + if appContext == "" { + appContext = stringValue(req.Current, "app_context") + } + grantedPermissions := req.Input.GetGrantedPermissions() + if len(grantedPermissions) == 0 { + grantedPermissions = stringSliceValue(req.Current, "granted_permissions") + } return &sdk.TypedStepResult[*contracts.ListContributionsOutput]{ - Output: &contracts.ListContributionsOutput{Contributions: module.registry.listForPermissions(req.Input.GetAppContext(), req.Input.GetGrantedPermissions())}, + Output: &contracts.ListContributionsOutput{Contributions: module.registry.listForPermissions(appContext, grantedPermissions)}, }, nil } diff --git a/internal/step_admin_test.go b/internal/step_admin_test.go index 9c01e28..25837e6 100644 --- a/internal/step_admin_test.go +++ b/internal/step_admin_test.go @@ -2,6 +2,7 @@ package internal import ( "context" + "errors" "testing" "github.com/GoCodeAlone/workflow-plugin-admin/internal/contracts" @@ -48,11 +49,15 @@ func TestAdminStepsRegisterListAuthorizeAndDispatch(t *testing.T) { if err != nil { t.Fatalf("list Execute: %v", err) } - contributions, ok := result.Output["contributions"].([]map[string]any) + contributions, ok := result.Output["contributions"].([]any) if !ok { t.Fatalf("contributions type = %T", result.Output["contributions"]) } - if len(contributions) != 1 || contributions[0]["id"] != "orders" { + first, ok := contributions[0].(map[string]any) + if !ok { + t.Fatalf("first contribution type = %T, want map[string]any", contributions[0]) + } + if len(contributions) != 1 || first["id"] != "orders" { t.Fatalf("unexpected contributions: %#v", contributions) } @@ -91,22 +96,66 @@ func TestAdminStepsRegisterListAuthorizeAndDispatch(t *testing.T) { } } -func TestAdminTypedStepProviderValidatesConfig(t *testing.T) { +func TestContributionRegistryStepsUseLegacyExecutionWithTypedContractDescriptors(t *testing.T) { provider := NewAdminPlugin().(sdk.TypedStepProvider) goodConfig, err := anypb.New(&contracts.AdminStepConfig{Module: "admin"}) if err != nil { t.Fatalf("pack good config: %v", err) } - if _, err := provider.CreateTypedStep("step.admin_list_contributions", "list", goodConfig); err != nil { - t.Fatalf("CreateTypedStep good config: %v", err) + if _, err := provider.CreateTypedStep("step.admin_register_contribution", "register", goodConfig); !errors.Is(err, sdk.ErrTypedContractNotHandled) { + t.Fatalf("CreateTypedStep register error = %v, want ErrTypedContractNotHandled", err) } + if _, err := provider.CreateTypedStep("step.admin_list_contributions", "list", goodConfig); !errors.Is(err, sdk.ErrTypedContractNotHandled) { + t.Fatalf("CreateTypedStep list error = %v, want ErrTypedContractNotHandled", err) + } + + steps := NewAdminPlugin().(sdk.TypedStepProvider).TypedStepTypes() + for _, stepType := range steps { + if stepType == "step.admin_register_contribution" || stepType == "step.admin_list_contributions" { + t.Fatalf("%s must use the legacy StepInstance path for map-shaped contribution arrays", stepType) + } + } +} + +func TestTypedAdminStepsUseCurrentFallbackForWorkflowInputs(t *testing.T) { + module := newDashboardModule("admin", &contracts.AdminDashboardConfig{}) + registerDashboardModule(module) - wrongConfig, err := anypb.New(&contracts.AdminDashboardConfig{RoutePrefix: "/admin"}) + registerResult, err := typedRegisterContribution(context.Background(), sdk.TypedStepRequest[*contracts.AdminStepConfig, *contracts.RegisterContributionInput]{ + Config: &contracts.AdminStepConfig{Module: "admin"}, + Input: &contracts.RegisterContributionInput{}, + Current: map[string]any{ + "contribution": map[string]any{ + "id": "authz-roles", + "title": "Authorization", + "path": "/admin/authz/", + "category": "security", + "render_mode": "iframe", + "app_context": "admin", + "permissions": []any{"admin:authz.roles:read"}, + }, + }, + }) + if err != nil { + t.Fatalf("typedRegisterContribution: %v", err) + } + if !registerResult.Output.GetRegistered() { + t.Fatal("typed register did not report registered") + } + + listResult, err := typedListContributions(context.Background(), sdk.TypedStepRequest[*contracts.AdminStepConfig, *contracts.ListContributionsInput]{ + Config: &contracts.AdminStepConfig{Module: "admin"}, + Input: &contracts.ListContributionsInput{}, + Current: map[string]any{ + "app_context": "admin", + "granted_permissions": []any{"admin:authz.roles:read"}, + }, + }) if err != nil { - t.Fatalf("pack wrong config: %v", err) + t.Fatalf("typedListContributions: %v", err) } - if _, err := provider.CreateTypedStep("step.admin_list_contributions", "list", wrongConfig); err == nil { - t.Fatal("CreateTypedStep accepted wrong config type") + if len(listResult.Output.GetContributions()) != 1 || listResult.Output.GetContributions()[0].GetId() != "authz-roles" { + t.Fatalf("unexpected typed list output: %#v", listResult.Output.GetContributions()) } } diff --git a/internal/ui_dist/index.html b/internal/ui_dist/index.html index ff1d1ee..f49031c 100644 --- a/internal/ui_dist/index.html +++ b/internal/ui_dist/index.html @@ -368,6 +368,7 @@

Management pages

const activeSurface = document.getElementById('active-surface'); const tokenStorageKey = shell.dataset.tokenStorageKey || 'workflow.admin.token'; const adminToolFrames = new WeakMap(); + let currentGrantedScopes = []; function storedToken() { try { @@ -415,6 +416,8 @@

Management pages

function adminToolGrants(sourceWindow) { const surface = adminToolFrames.get(sourceWindow); + if (surface && Array.isArray(surface.grantedScopes)) return surface.grantedScopes; + if (currentGrantedScopes.length) return currentGrantedScopes; if (!surface || !Array.isArray(surface.permissions)) return []; return surface.permissions .map(permission => permission && permission.permission) @@ -434,9 +437,12 @@

Management pages

}, event.origin); }); - function showPanel(id, label) { + function showPanel(id, label, selectedItem = null) { document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id === id)); - document.querySelectorAll('.nav-item').forEach(item => item.classList.toggle('active', item.dataset.panel === id)); + document.querySelectorAll('.nav-item').forEach(item => { + const active = selectedItem ? item === selectedItem : item.dataset.panel === id; + item.classList.toggle('active', active); + }); title.textContent = label; activeSurface.textContent = label; } @@ -466,8 +472,16 @@

Management pages

item.addEventListener('click', () => showPanel(item.dataset.panel, item.textContent.trim())); }); - function renderAdminTools(contributions) { + function renderAdminTools(contributions, grantedScopes = []) { nav.textContent = ''; + const fallbackScopes = contributions.flatMap(surface => { + const permissions = Array.isArray(surface.permissions) ? surface.permissions : []; + return permissions.map(permission => permission && permission.permission).filter(Boolean); + }); + currentGrantedScopes = Array.from(new Set((grantedScopes.length ? grantedScopes : fallbackScopes).filter(Boolean))); + contributions.forEach(surface => { + surface.grantedScopes = currentGrantedScopes; + }); surfaceCount.textContent = String(contributions.length); const categories = Array.from(new Set(contributions.map(surface => surface.category || 'plugin'))); categoryCount.textContent = String(categories.length); @@ -494,7 +508,7 @@

Management pages

button.className = 'nav-item'; button.dataset.panel = 'contributions-panel'; button.textContent = surface.title || surface.id || surface.path || 'Untitled surface'; - button.addEventListener('click', () => openContribution(surface)); + button.addEventListener('click', () => openContribution(surface, button)); nav.appendChild(button); }); }); @@ -636,8 +650,8 @@

Management pages

list.replaceChildren(form); } - function openContribution(surface) { - showPanel('contributions-panel', surface.title || surface.id || 'Contribution'); + function openContribution(surface, selectedItem = null) { + showPanel('contributions-panel', surface.title || surface.id || 'Contribution', selectedItem); if (surface.render_mode === 'config-form') { list.className = 'empty'; list.textContent = 'Loading configuration...'; @@ -688,10 +702,10 @@

Management pages

} if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = await response.json(); - renderAdminTools(payload.contributions || []); + renderAdminTools(payload.contributions || [], payload.granted_permissions || payload.grantedPermissions || []); status.textContent = 'Admin tools loaded'; } catch (error) { - renderAdminTools([]); + renderAdminTools([], []); status.textContent = 'Offline fallback'; } } diff --git a/plugin.contracts.json b/plugin.contracts.json index 4cb5bf7..99ef061 100644 --- a/plugin.contracts.json +++ b/plugin.contracts.json @@ -10,7 +10,7 @@ { "kind": "step", "type": "step.admin_register_contribution", - "mode": "strict", + "mode": "proto_with_legacy_struct", "config": "workflow.plugins.admin.v1.AdminStepConfig", "input": "workflow.plugins.admin.v1.RegisterContributionInput", "output": "workflow.plugins.admin.v1.RegisterContributionOutput" @@ -18,7 +18,7 @@ { "kind": "step", "type": "step.admin_list_contributions", - "mode": "strict", + "mode": "proto_with_legacy_struct", "config": "workflow.plugins.admin.v1.AdminStepConfig", "input": "workflow.plugins.admin.v1.ListContributionsInput", "output": "workflow.plugins.admin.v1.ListContributionsOutput" @@ -77,4 +77,3 @@ } ] } - From 455e01515c9e09529f7a4837d4bef6886a48b65d Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Mon, 1 Jun 2026 02:40:01 -0400 Subject: [PATCH 2/2] fix: address admin bridge review --- internal/registry.go | 11 +++++------ internal/ui_dist/index.html | 15 ++++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/registry.go b/internal/registry.go index 5c95199..b6ccacb 100644 --- a/internal/registry.go +++ b/internal/registry.go @@ -50,19 +50,18 @@ func (r *contributionRegistry) list(appContext string) []*contracts.AdminContrib } func (r *contributionRegistry) listForPermissions(appContext string, grantedPermissions []string) []*contracts.AdminContribution { - r.mu.RLock() - defer r.mu.RUnlock() - grants := make(map[string]struct{}, len(grantedPermissions)) for _, permission := range grantedPermissions { grants[permission] = struct{}{} } - out := make([]*contracts.AdminContribution, 0, len(r.contributions)) + r.mu.RLock() + snapshot := make([]*contracts.AdminContribution, 0, len(r.contributions)) for _, contribution := range r.contributions { - out = append(out, proto.Clone(contribution).(*contracts.AdminContribution)) + snapshot = append(snapshot, contribution) } - return filterContributionList(out, appContext, grants) + r.mu.RUnlock() + return filterContributionList(snapshot, appContext, grants) } func filterContributionList(contributions []*contracts.AdminContribution, appContext string, grants map[string]struct{}) []*contracts.AdminContribution { diff --git a/internal/ui_dist/index.html b/internal/ui_dist/index.html index f49031c..f187962 100644 --- a/internal/ui_dist/index.html +++ b/internal/ui_dist/index.html @@ -401,6 +401,12 @@

Management pages

return headers; } + function scopeStrings(values) { + return Array.isArray(values) + ? values.filter(value => typeof value === 'string' && value) + : []; + } + function isAllowedAdminToolSource(sourceWindow) { if (!sourceWindow) return false; for (const frame of document.querySelectorAll('iframe.surface-frame')) { @@ -419,9 +425,7 @@

Management pages

if (surface && Array.isArray(surface.grantedScopes)) return surface.grantedScopes; if (currentGrantedScopes.length) return currentGrantedScopes; if (!surface || !Array.isArray(surface.permissions)) return []; - return surface.permissions - .map(permission => permission && permission.permission) - .filter(permission => typeof permission === 'string' && permission); + return scopeStrings(surface.permissions.map(permission => permission && permission.permission)); } window.addEventListener('message', event => { @@ -476,9 +480,10 @@

Management pages

nav.textContent = ''; const fallbackScopes = contributions.flatMap(surface => { const permissions = Array.isArray(surface.permissions) ? surface.permissions : []; - return permissions.map(permission => permission && permission.permission).filter(Boolean); + return scopeStrings(permissions.map(permission => permission && permission.permission)); }); - currentGrantedScopes = Array.from(new Set((grantedScopes.length ? grantedScopes : fallbackScopes).filter(Boolean))); + const explicitScopes = scopeStrings(grantedScopes); + currentGrantedScopes = Array.from(new Set(explicitScopes.length ? explicitScopes : fallbackScopes)); contributions.forEach(surface => { surface.grantedScopes = currentGrantedScopes; });