diff --git a/docs/enterprise-configuration.md b/docs/enterprise-configuration.md new file mode 100644 index 0000000000..da05d6dfd8 --- /dev/null +++ b/docs/enterprise-configuration.md @@ -0,0 +1,236 @@ +# Custom API Endpoint Configuration + +This guide explains how to configure GitHub Agentic Workflows to use custom API endpoints for GitHub Enterprise Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints. + +## Overview + +GitHub Agentic Workflows supports custom API endpoints through the `engine.api-target` configuration field. This allows you to specify custom endpoints for: + +- **GitHub Enterprise Cloud (GHEC)** - Tenant-specific Copilot API endpoints +- **GitHub Enterprise Server (GHES)** - Enterprise Copilot API endpoints +- **Custom AI Endpoints** - Custom OpenAI-compatible or Anthropic-compatible endpoints + +## Configuration + +To configure a custom API endpoint, add the `api-target` field to your engine configuration: + +**Basic Configuration:** + +```yaml +--- +engine: + id: copilot + api-target: api.acme.ghe.com +network: + allowed: + - defaults + - acme.ghe.com + - api.acme.ghe.com +--- +``` + +The `api-target` field accepts a hostname (without protocol or path) and works with any agentic engine. + +## Examples + +### GitHub Enterprise Cloud (GHEC) + +For GHEC tenants (domains ending with `.ghe.com`), specify your tenant-specific API endpoint: + +**Workflow Configuration:** + +```yaml +--- +engine: + id: copilot + api-target: api.acme.ghe.com +network: + allowed: + - defaults + - acme.ghe.com + - api.acme.ghe.com +--- +``` + +**Required domains in network allowlist:** +- `acme.ghe.com` - Your GHEC tenant domain (git operations, web UI) +- `api.acme.ghe.com` - Your tenant-specific Copilot API endpoint +- `raw.githubusercontent.com` - Raw content access (if using GitHub MCP server) + +### GitHub Enterprise Server (GHES) + +For GHES instances (custom domains), specify the enterprise Copilot endpoint: + +**Workflow Configuration:** + +```yaml +--- +engine: + id: copilot + api-target: api.enterprise.githubcopilot.com +network: + allowed: + - defaults + - github.company.com + - api.enterprise.githubcopilot.com +--- +``` + +**Required domains in network allowlist:** +- `github.company.com` - Your GHES instance (git operations, web UI) +- `api.enterprise.githubcopilot.com` - Enterprise Copilot API endpoint (used for all GHES instances) + +### Custom AI Endpoints + +The `api-target` field works with any agentic engine, allowing you to use custom AI endpoints: + +**Workflow Configuration:** + +```yaml +--- +engine: + id: codex + api-target: api.custom.ai-provider.com +network: + allowed: + - defaults + - api.custom.ai-provider.com +--- +``` + +## Complete Examples + +### GHEC with GitHub MCP Server + +```yaml +--- +description: Workflow for GHEC environment with GitHub API access +on: + workflow_dispatch: +permissions: + contents: read + issues: write + pull-requests: write +engine: + id: copilot + api-target: api.acme.ghe.com +tools: + github: + mode: remote + toolsets: [default] +network: + allowed: + - defaults + - acme.ghe.com + - api.acme.ghe.com + - raw.githubusercontent.com +--- + +# Your workflow prompt here +``` + +### GHES with Custom Endpoint + +```yaml +--- +description: Workflow for GHES environment +on: + issue_comment: + types: [created] +permissions: + contents: read + issues: write +engine: + id: copilot + api-target: api.enterprise.githubcopilot.com +network: + allowed: + - defaults + - github.company.com + - api.enterprise.githubcopilot.com +--- + +# Your workflow prompt here +``` + +### Custom AI Provider + +```yaml +--- +description: Workflow with custom AI endpoint +on: + workflow_dispatch: +permissions: + contents: read +engine: + id: codex + api-target: api.custom.ai-provider.com +network: + allowed: + - defaults + - api.custom.ai-provider.com +--- + +# Your workflow prompt here +``` + +## Verification + +To verify your configuration is working correctly: + +### 1. Check Compiled Workflow + +After compiling your workflow, check the generated `.lock.yml` file: + +```bash +gh aw compile your-workflow.md +``` + +Look for: +- `--copilot-api-target` flag in AWF command (if using Copilot engine) +- Correct API endpoint hostname in the flag value + +### 2. Check Workflow Runs + +In GitHub Actions workflow runs: +1. Go to the agent job +2. Check the "Run Copilot Agent" (or equivalent) step +3. Verify the AWF command includes the correct API target +4. Check AWF logs for API connection messages + +## Troubleshooting + +### Wrong API Endpoint + +**Problem:** Traffic is going to the wrong API endpoint + +**Solutions:** +1. Verify `engine.api-target` is set correctly in your workflow frontmatter +2. Check that the domain is in your `network.allowed` list +3. Review AWF logs in the workflow run for endpoint configuration messages +4. Ensure you're not using a full URL (use hostname only: `api.acme.ghe.com` not `https://api.acme.ghe.com`) + +### Domain Not Whitelisted + +**Problem:** Requests are blocked with network errors + +**Solution:** Add the missing domain to your `network.allowed` list: +- For GHEC: `[acme.ghe.com, api.acme.ghe.com]` +- For GHES: `[github.company.com, api.enterprise.githubcopilot.com]` +- For custom AI: `[api.custom.ai-provider.com]` + +### GitHub MCP Server Issues + +**Problem:** GitHub MCP server fails to connect to your enterprise instance + +**Solutions:** +1. Ensure your GHEC/GHES domain is in `network.allowed` +2. Verify the GitHub token has appropriate scopes for your enterprise tenant +3. Use `mode: remote` for the GitHub MCP server when on GHEC/GHES + +## Related Documentation + +- [AWF Firewall Configuration](https://github.com/github/gh-aw-firewall) - Detailed AWF documentation +- [GitHub Actions Environment Variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) - Default GitHub Actions variables +- [Network Permissions](network.md) - Network access configuration +- [Tools Configuration](tools.md) - MCP server and tool setup diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index e331e1e95b..af31aac657 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -7971,6 +7971,11 @@ "type": "string", "description": "Agent identifier to pass to copilot --agent flag (copilot engine only). Specifies which custom agent to use for the workflow." }, + "api-target": { + "type": "string", + "description": "Custom API endpoint hostname for the agentic engine. Used for GitHub Enterprise Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints. Example: 'api.acme.ghe.com' for GHEC, 'api.enterprise.githubcopilot.com' for GHES, or custom endpoint hostnames.", + "examples": ["api.acme.ghe.com", "api.enterprise.githubcopilot.com", "api.custom.endpoint.com"] + }, "args": { "type": "array", "items": { diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index fe84d182ab..b59525badc 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -205,6 +205,13 @@ func BuildAWFArgs(config AWFCommandConfig) []string { awfHelpersLog.Printf("Added --anthropic-api-target=%s", anthropicTarget) } + // Add Copilot API target for custom Copilot endpoints (GHEC, GHES, or custom) + // This uses the engine.api-target field if configured + if config.WorkflowData.EngineConfig != nil && config.WorkflowData.EngineConfig.APITarget != "" { + awfArgs = append(awfArgs, "--copilot-api-target", config.WorkflowData.EngineConfig.APITarget) + awfHelpersLog.Printf("Added --copilot-api-target=%s", config.WorkflowData.EngineConfig.APITarget) + } + // Add SSL Bump support for HTTPS content inspection (v0.9.0+) sslBumpArgs := getSSLBumpArgs(firewallConfig) awfArgs = append(awfArgs, sslBumpArgs...) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 184d8e854d..ceeeb0d406 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -259,6 +259,7 @@ COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" // Always add GH_AW_PROMPT for agentic workflows env["GH_AW_PROMPT"] = "/tmp/gh-aw/aw-prompts/prompt.txt" + // Tag the step as a GitHub AW agentic execution for discoverability by agents env["GITHUB_AW"] = "true" // Indicate the phase: "agent" for the main run, "detection" for threat detection diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 0149a6cdfb..76955165d6 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -26,6 +26,7 @@ type EngineConfig struct { Args []string Firewall *FirewallConfig // AWF firewall configuration Agent string // Agent identifier for copilot --agent flag (copilot engine only) + APITarget string // Custom API endpoint hostname (e.g., "api.acme.ghe.com" or "api.enterprise.githubcopilot.com") // Inline definition fields (populated when engine.runtime is specified in frontmatter) IsInlineDefinition bool // true when the engine is defined inline via engine.runtime + optional engine.provider @@ -314,6 +315,14 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng } } + // Extract optional 'api-target' field (custom API endpoint for any engine) + if apiTarget, hasAPITarget := engineObj["api-target"]; hasAPITarget { + if apiTargetStr, ok := apiTarget.(string); ok && apiTargetStr != "" { + config.APITarget = apiTargetStr + engineLog.Printf("Extracted api-target: %s", apiTargetStr) + } + } + // Return the ID as the engineSetting for backwards compatibility engineLog.Printf("Extracted engine configuration: ID=%s", config.ID) return config.ID, config diff --git a/pkg/workflow/engine_test.go b/pkg/workflow/engine_test.go index 34e4ba7d5a..d8f65c5c83 100644 --- a/pkg/workflow/engine_test.go +++ b/pkg/workflow/engine_test.go @@ -250,3 +250,79 @@ func TestEngineCommandField(t *testing.T) { }) } } + +// TestAPITargetExtraction tests that the api-target configuration is correctly +// extracted from frontmatter for custom API endpoints (GHEC, GHES, or custom AI endpoints). +func TestAPITargetExtraction(t *testing.T) { + tests := []struct { + name string + frontmatter map[string]any + expectedAPITarget string + }{ + { + name: "GHEC api-target", + frontmatter: map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "api-target": "api.acme.ghe.com", + }, + }, + expectedAPITarget: "api.acme.ghe.com", + }, + { + name: "GHES api-target", + frontmatter: map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "api-target": "api.enterprise.githubcopilot.com", + }, + }, + expectedAPITarget: "api.enterprise.githubcopilot.com", + }, + { + name: "custom api-target", + frontmatter: map[string]any{ + "engine": map[string]any{ + "id": "codex", + "api-target": "api.custom.endpoint.com", + }, + }, + expectedAPITarget: "api.custom.endpoint.com", + }, + { + name: "no api-target", + frontmatter: map[string]any{ + "engine": map[string]any{ + "id": "copilot", + }, + }, + expectedAPITarget: "", + }, + { + name: "empty api-target", + frontmatter: map[string]any{ + "engine": map[string]any{ + "id": "copilot", + "api-target": "", + }, + }, + expectedAPITarget: "", + }, + } + + compiler := NewCompiler() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, config := compiler.ExtractEngineConfig(tt.frontmatter) + + if config == nil { + t.Fatal("Expected config to be non-nil") + } + + if config.APITarget != tt.expectedAPITarget { + t.Errorf("Expected api-target %q, got %q", tt.expectedAPITarget, config.APITarget) + } + }) + } +} diff --git a/pkg/workflow/secrets_validation.go b/pkg/workflow/secrets_validation.go index 5e9e3dbc83..50aee2ad6b 100644 --- a/pkg/workflow/secrets_validation.go +++ b/pkg/workflow/secrets_validation.go @@ -12,9 +12,6 @@ var secretsValidationLog = newValidationLogger("secrets") // This is the same pattern used in the github_token schema definition ($defs/github_token). var secretsExpressionPattern = regexp.MustCompile(`^\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*(\s*\|\|\s*secrets\.[A-Za-z_][A-Za-z0-9_]*)*\s*\}\}$`) -// secretNamePattern validates that a secret name follows environment variable naming conventions -var secretNamePattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) - // validateSecretsExpression validates that a value is a proper GitHub Actions secrets expression. // Returns an error if the value is not in the format: ${{ secrets.NAME }} or ${{ secrets.NAME || secrets.NAME2 }} // Note: This function intentionally does not accept the secret key name as a parameter to prevent