Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkg/cli/audit_report_experiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -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{
Expand Down
9 changes: 9 additions & 0 deletions pkg/cli/logs_safe_output_chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down
9 changes: 9 additions & 0 deletions pkg/cli/mcp_argument_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 ""
}

Expand Down
21 changes: 19 additions & 2 deletions pkg/workflow/awf_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
}
}
Expand All @@ -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

Expand All @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/safe_outputs_needs_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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,
Expand All @@ -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,
Expand Down