From 6ce4f9e76465e3d50a9b0afdaa5cdf2d73e5bd9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 23:35:50 +0000 Subject: [PATCH 1/2] Initial plan From 2aef7271a8f80d0028bd97ed6662a58000c02b45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 23:49:20 +0000 Subject: [PATCH 2/2] Optimize permission scope validation caching in workflow compiler Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/compiler_types.go | 2 ++ pkg/workflow/compiler_validators_test.go | 21 +++++++++++++++++++ .../permissions_compiler_validator.go | 10 +++++++-- pkg/workflow/tools.go | 2 ++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 16d865c5aaf..44bfe85b84e 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -570,6 +570,8 @@ type WorkflowData struct { ServicePortExpressions string // comma-separated ${{ job.services[''].ports[''] }} expressions for AWF --allow-host-service-ports RunInstallScripts bool // true when run-install-scripts: true is set (globally or per node runtime); disables --ignore-scripts on generated npm install steps CachedPermissions *Permissions // cached parsed Permissions object (for performance optimization); populated by applyDefaults after all permission mutations + CachedPermissionScopeNamesErr error // cached result of ValidatePermissionScopeNames(Permissions); nil = valid; populated by applyDefaults + CachedPermissionScopeNamesSet bool // true once CachedPermissionScopeNamesErr has been populated; distinguishes "valid (nil)" from "not yet computed" ConcurrencyGroupExpr string // cached concurrency group expression extracted from Concurrency YAML (for performance optimization); populated by applyDefaults CachedConcurrencyGroupExprErr error // cached result of validateConcurrencyGroupExpression(ConcurrencyGroupExpr); nil = valid; populated by applyDefaults Experiments map[string][]string // A/B testing experiments: maps experiment name to variant list (from frontmatter) diff --git a/pkg/workflow/compiler_validators_test.go b/pkg/workflow/compiler_validators_test.go index 82c4e531cfb..a537c8096a2 100644 --- a/pkg/workflow/compiler_validators_test.go +++ b/pkg/workflow/compiler_validators_test.go @@ -3,6 +3,7 @@ package workflow import ( + "errors" "path/filepath" "testing" @@ -310,6 +311,26 @@ func TestValidateToolConfiguration(t *testing.T) { } } +func TestValidatePermissions_UsesCachedPermissionScopeValidation(t *testing.T) { + tmpDir := testutil.TempDir(t, "perms-cache-test") + markdownPath := filepath.Join(tmpDir, "test.md") + + cachedErr := errors.New("cached permission scope validation failure") + workflowData := &WorkflowData{ + Name: "Test", + MarkdownContent: "# Test", + AI: "copilot", + Permissions: "permissions:\n contents: read\n", + CachedPermissionScopeNamesSet: true, + CachedPermissionScopeNamesErr: cachedErr, + } + + compiler := NewCompiler() + _, err := compiler.validatePermissions(workflowData, markdownPath) + require.Error(t, err) + assert.Contains(t, err.Error(), cachedErr.Error()) +} + // TestWarnPromptTmpPaths tests the /tmp path heuristic used by the compiler. func TestWarnPromptTmpPaths(t *testing.T) { tests := []struct { diff --git a/pkg/workflow/permissions_compiler_validator.go b/pkg/workflow/permissions_compiler_validator.go index 6900b2f23a4..350cd5a0bd9 100644 --- a/pkg/workflow/permissions_compiler_validator.go +++ b/pkg/workflow/permissions_compiler_validator.go @@ -60,8 +60,14 @@ func (c *Compiler) validatePermissions(workflowData *WorkflowData, markdownPath // Validate permission scope names for typos (e.g. "contnts" → "contents") workflowLog.Printf("Validating permission scope names") - if err := ValidatePermissionScopeNames(workflowData.Permissions); err != nil { - return nil, formatCompilerError(markdownPath, "error", err.Error(), err) + var scopeValidationErr error + if workflowData.CachedPermissionScopeNamesSet { + scopeValidationErr = workflowData.CachedPermissionScopeNamesErr + } else { + scopeValidationErr = ValidatePermissionScopeNames(workflowData.Permissions) + } + if scopeValidationErr != nil { + return nil, formatCompilerError(markdownPath, "error", scopeValidationErr.Error(), scopeValidationErr) } // Validate dangerous permissions diff --git a/pkg/workflow/tools.go b/pkg/workflow/tools.go index 5e9d445b53a..56ff1125c44 100644 --- a/pkg/workflow/tools.go +++ b/pkg/workflow/tools.go @@ -28,6 +28,8 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) error // YAML parsing, regex extraction, and expression parsing in the hot validateWorkflowData loop. defer func() { data.CachedPermissions = NewPermissionsParser(data.Permissions).ToPermissions() + data.CachedPermissionScopeNamesErr = ValidatePermissionScopeNames(data.Permissions) + data.CachedPermissionScopeNamesSet = true data.ConcurrencyGroupExpr = extractConcurrencyGroupFromYAML(data.Concurrency) // Pre-validate and cache the concurrency group expression so validateWorkflowData // can short-circuit without re-running the expensive ExpressionParser on every call.