diff --git a/pkg/cli/audit_report_experiments.go b/pkg/cli/audit_report_experiments.go index bc7fb304ee8..e7aea6f59f4 100644 --- a/pkg/cli/audit_report_experiments.go +++ b/pkg/cli/audit_report_experiments.go @@ -14,8 +14,11 @@ import ( "strings" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/logger" ) +var experimentDataLog = logger.New("cli:audit_report_experiments") + // ExperimentData represents the A/B experiment assignments for a single workflow run. type ExperimentData struct { // Assignments maps each experiment name to the variant selected for this run. @@ -61,11 +64,15 @@ func extractExperimentData(logsPath string) *ExperimentData { return nil } + experimentDataLog.Printf("Extracting experiment data from: %s", logsPath) + statePath := findExperimentStatePath(logsPath) if statePath == "" { + experimentDataLog.Print("No experiment state file found") return nil } + experimentDataLog.Printf("Reading experiment state from: %s", statePath) raw, err := os.ReadFile(statePath) if err != nil { return nil @@ -76,6 +83,8 @@ func extractExperimentData(logsPath string) *ExperimentData { return nil } + experimentDataLog.Printf("Found %d experiment(s) in state file", len(state.Counts)) + // Derive this-run assignments: the variant selected on the most-recent run is // the one with the maximum count (ties resolved by sorted order). assignments := make(map[string]string, len(state.Counts)) @@ -89,6 +98,7 @@ func extractExperimentData(logsPath string) *ExperimentData { variantCounts := state.Counts[name] selected := deriveLastSelectedVariant(variantCounts) assignments[name] = selected + experimentDataLog.Printf("Experiment %q: selected variant=%q", name, selected) } return &ExperimentData{ diff --git a/pkg/cli/logs_safe_output_chains.go b/pkg/cli/logs_safe_output_chains.go index 5ab484d2cc6..0158ca10544 100644 --- a/pkg/cli/logs_safe_output_chains.go +++ b/pkg/cli/logs_safe_output_chains.go @@ -7,8 +7,11 @@ import ( "strconv" "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/logger" ) +var safeOutputChainsLog = logger.New("cli:logs_safe_output_chains") + const ( temporaryIDMapStatusLoaded = "loaded" temporaryIDMapStatusMissing = "missing" @@ -42,12 +45,16 @@ func buildSafeOutputChainMetrics(logsPath string) SafeOutputChainMetrics { return metrics } if !safeOutputArtifactsPresent(logsPath) { + safeOutputChainsLog.Print("No safe output artifacts present, skipping chain metrics") return metrics } + safeOutputChainsLog.Printf("Building safe output chain metrics: manifest_entries=%d", len(items)) + resolvedTargets, tempIDMapStatus := loadResolvedTemporaryIDTargets(logsPath) metrics.TemporaryIDMapStatus = tempIDMapStatus metrics.TemporaryIDMappings = len(resolvedTargets) + safeOutputChainsLog.Printf("Temporary ID map status=%s, mappings=%d", tempIDMapStatus, len(resolvedTargets)) if len(items) == 0 || len(resolvedTargets) == 0 { return metrics } @@ -84,6 +91,8 @@ func buildSafeOutputChainMetrics(logsPath string) SafeOutputChainMetrics { } } + safeOutputChainsLog.Printf("Chain metrics computed: chained_targets=%d, followup_actions=%d, delegated=%d, closed=%d", + metrics.ChainedTargetCount, metrics.ChainedFollowupActionCount, metrics.DelegatedTempTargetCount, metrics.ClosedTempTargetCount) return metrics } diff --git a/pkg/cli/mcp_argument_validation.go b/pkg/cli/mcp_argument_validation.go index 5422d1c0a99..864a7d7669a 100644 --- a/pkg/cli/mcp_argument_validation.go +++ b/pkg/cli/mcp_argument_validation.go @@ -20,9 +20,12 @@ import ( "sort" "strings" + "github.com/github/gh-aw/pkg/logger" "github.com/modelcontextprotocol/go-sdk/mcp" ) +var mcpArgValidationLog = logger.New("cli:mcp_argument_validation") + // toolParamEntry holds the valid parameter names for a single MCP tool. type toolParamEntry = []string @@ -72,6 +75,8 @@ func argumentValidationMiddleware(toolParams map[string]toolParamEntry) mcp.Midd toolName := extractMCPToolName(req) validParams := toolParams[toolName] + mcpArgValidationLog.Printf("Intercepted unknown param error: tool=%s, unknown_params=%v", toolName, unknownParams) + // Build a helpful replacement message. helpMsg := buildHelpfulParamError(toolName, unknownParams, validParams) @@ -156,6 +161,8 @@ func findSimilarParam(unknown string, validParams []string) string { return "" } + mcpArgValidationLog.Printf("Finding similar param for %q among %d candidates", unknown, len(validParams)) + normUnknown := normalizeParamName(unknown) type candidate struct { @@ -185,8 +192,10 @@ func findSimilarParam(unknown string, validParams []string) string { const threshold = 0.7 if best.score >= threshold { + mcpArgValidationLog.Printf("Found similar param: %q -> %q (score=%.2f)", unknown, best.name, best.score) return best.name } + mcpArgValidationLog.Printf("No similar param found for %q (best score=%.2f, threshold=%.2f)", unknown, best.score, threshold) return "" } diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index bea6266353f..3f4678f5a88 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -47,8 +47,12 @@ import ( "encoding/json" "fmt" "strings" + + "github.com/github/gh-aw/pkg/logger" ) +var awfConfigLog = logger.New("workflow:awf_config") + // AWFConfigFile represents the AWF configuration file schema. // This is the top-level structure written to awf-config.json. type AWFConfigFile struct { @@ -113,21 +117,27 @@ type AWFContainerConfig struct { // The caller is responsible for writing the returned JSON to disk at the path expected // by the AWF --config flag. See BuildAWFCommand for how this is wired together. func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { + awfConfigLog.Printf("Building AWF config JSON: engine=%s, allowed_domains=%q", config.EngineName, config.AllowedDomains) + awfConfig := AWFConfigFile{ Schema: "https://github.com/github/gh-aw-firewall/schemas/awf-config.v1.json", } // ── Network section ────────────────────────────────────────────────────── if config.AllowedDomains != "" { + allowList := splitDomainList(config.AllowedDomains) awfConfig.Network = &AWFNetworkConfig{ - AllowDomains: splitDomainList(config.AllowedDomains), + AllowDomains: allowList, } + awfConfigLog.Printf("Network section: %d allowed domains", len(allowList)) // Blocked domains (if configured in the workflow) if config.WorkflowData != nil { blockedDomainsStr := formatBlockedDomains(config.WorkflowData.NetworkPermissions) if blockedDomainsStr != "" { - awfConfig.Network.BlockDomains = splitDomainList(blockedDomainsStr) + blockList := splitDomainList(blockedDomainsStr) + awfConfig.Network.BlockDomains = blockList + awfConfigLog.Printf("Network section: %d blocked domains", len(blockList)) } } } @@ -141,19 +151,24 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { if openaiTarget := extractAPITargetHost(config.WorkflowData, "OPENAI_BASE_URL"); openaiTarget != "" { targets["openai"] = &AWFAPITargetConfig{Host: openaiTarget} + awfConfigLog.Printf("API proxy: custom openai target=%s", openaiTarget) } if anthropicTarget := extractAPITargetHost(config.WorkflowData, "ANTHROPIC_BASE_URL"); anthropicTarget != "" { targets["anthropic"] = &AWFAPITargetConfig{Host: anthropicTarget} + awfConfigLog.Printf("API proxy: custom anthropic target=%s", anthropicTarget) } if copilotTarget := GetCopilotAPITarget(config.WorkflowData); copilotTarget != "" { targets["copilot"] = &AWFAPITargetConfig{Host: copilotTarget} + awfConfigLog.Printf("API proxy: custom copilot target=%s", copilotTarget) } if geminiTarget := GetGeminiAPITarget(config.WorkflowData, config.EngineName); geminiTarget != "" { targets["gemini"] = &AWFAPITargetConfig{Host: geminiTarget} + awfConfigLog.Printf("API proxy: custom gemini target=%s", geminiTarget) } if len(targets) > 0 { apiProxy.Targets = targets + awfConfigLog.Printf("API proxy: %d custom targets configured", len(targets)) } awfConfig.APIProxy = apiProxy @@ -164,12 +179,14 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { awfConfig.Container = &AWFContainerConfig{ ImageTag: awfImageTag, } + awfConfigLog.Printf("Container section: image_tag=%s", awfImageTag) } jsonBytes, err := json.Marshal(awfConfig) if err != nil { return "", fmt.Errorf("failed to marshal AWF config to JSON: %w", err) } + awfConfigLog.Printf("AWF config JSON generated: %d bytes", len(jsonBytes)) return string(jsonBytes), nil } diff --git a/pkg/workflow/safe_outputs_needs_validation.go b/pkg/workflow/safe_outputs_needs_validation.go index 5280779c41b..954051b81a0 100644 --- a/pkg/workflow/safe_outputs_needs_validation.go +++ b/pkg/workflow/safe_outputs_needs_validation.go @@ -14,6 +14,8 @@ func validateSafeOutputsNeeds(data *WorkflowData) error { return nil } + safeOutputsNeedsValidationLog.Printf("Validating safe-outputs needs: %d need(s) declared", len(data.SafeOutputs.Needs)) + if err := validateSafeOutputsNeedsField(data, "needs", data.SafeOutputs.Needs); err != nil { return err } @@ -33,9 +35,11 @@ func validateSafeOutputsNeedsField(data *WorkflowData, fieldName string, needs [ } customJobs[jobName] = true } + safeOutputsNeedsValidationLog.Printf("Found %d custom job(s) available as needs targets", len(customJobs)) for _, need := range needs { if isReservedSafeOutputsNeedsTarget(need) { + safeOutputsNeedsValidationLog.Printf("Validation failed: %q is a reserved job name", need) return fmt.Errorf( "safe-outputs.%s: built-in job %q is not allowed. Expected one of the workflow's custom jobs. Example: safe-outputs.%s: [secrets_fetcher]", fieldName, @@ -44,6 +48,7 @@ func validateSafeOutputsNeedsField(data *WorkflowData, fieldName string, needs [ ) } if !customJobs[need] { + safeOutputsNeedsValidationLog.Printf("Validation failed: %q is not a known custom job", need) return fmt.Errorf( "safe-outputs.%s: unknown job %q. Expected one of the workflow's custom jobs. Example: safe-outputs.%s: [secrets_fetcher]", fieldName,