diff --git a/.github/workflows/design-decision-gate.lock.yml b/.github/workflows/design-decision-gate.lock.yml index f50dc0a3d6b..8b144e8eccc 100644 --- a/.github/workflows/design-decision-gate.lock.yml +++ b/.github/workflows/design-decision-gate.lock.yml @@ -797,14 +797,10 @@ jobs: timeout-minutes: 15 run: | set -o pipefail - # Extract markdown body from custom agent file (skip frontmatter) - AGENT_CONTENT="$(awk 'BEGIN{skip=1} /^---$/{if(skip){skip=0;next}else{skip=1;next}} !skip' "${GITHUB_WORKSPACE}/.github/agents/adr-writer.agent.md")" - # Combine agent content with prompt - PROMPT_TEXT="$(printf '%s\n\n%s' "$AGENT_CONTENT" "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)")" touch /tmp/gh-aw/agent-step-summary.md # shellcheck disable=SC1003 sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --tty --env-all --exclude-env ANTHROPIC_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --no-chrome --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash(cat),Bash(cat:*),Bash(date),Bash(echo),Bash(echo:*),Bash(find:*),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git diff:*),Bash(git log:*),Bash(git merge:*),Bash(git rm:*),Bash(git show:*),Bash(git status),Bash(git switch:*),Bash(grep),Bash(grep:*),Bash(head),Bash(ls),Bash(ls:*),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(wc:*),Bash(yq),BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$PROMPT_TEXT"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --no-chrome --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools '\''Bash(cat),Bash(cat:*),Bash(date),Bash(echo),Bash(echo:*),Bash(find:*),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git diff:*),Bash(git log:*),Bash(git merge:*),Bash(git rm:*),Bash(git show:*),Bash(git status),Bash(git switch:*),Bash(grep),Bash(grep:*),Bash(head),Bash(ls),Bash(ls:*),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(wc:*),Bash(yq),BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index 2f8ced786e2..c1e41d441cf 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -754,14 +754,10 @@ jobs: timeout-minutes: 45 run: | set -o pipefail - # Extract markdown body from custom agent file (skip frontmatter) - AGENT_CONTENT="$(awk 'BEGIN{skip=1} /^---$/{if(skip){skip=0;next}else{skip=1;next}} !skip' "${GITHUB_WORKSPACE}/.github/agents/ci-cleaner.agent.md")" - # Combine agent content with prompt - PROMPT_TEXT="$(printf '%s\n\n%s' "$AGENT_CONTENT" "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)")" touch /tmp/gh-aw/agent-step-summary.md # shellcheck disable=SC1003 sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --tty --env-all --exclude-env ANTHROPIC_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --mount /opt/hostedtoolcache/go:/opt/hostedtoolcache/go:ro --mount /usr/bin/go:/usr/bin/go:ro --mount /usr/bin/make:/usr/bin/make:ro --mount /usr/local/bin/node:/usr/local/bin/node:ro --mount /usr/local/bin/npm:/usr/local/bin/npm:ro --mount /usr/local/lib/node_modules:/usr/local/lib/node_modules:ro --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,go.dev,golang.org,goproxy.io,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pkg.go.dev,playwright.download.prss.microsoft.com,ppa.launchpad.net,proxy.golang.org,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,storage.googleapis.com,sum.golang.org,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --no-chrome --max-turns 20 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$PROMPT_TEXT"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --no-chrome --max-turns 20 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 diff --git a/pkg/agentdrain/data/default_weights.json b/pkg/agentdrain/data/default_weights.json index f232d2e905d..74ea4e2b819 100644 --- a/pkg/agentdrain/data/default_weights.json +++ b/pkg/agentdrain/data/default_weights.json @@ -100,12 +100,7 @@ ], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -151,21 +146,12 @@ "id": 1, "size": 100, "stage": "finish", - "template": [ - "stage=finish", - "\u003c*\u003e", - "tokens=\u003cNUM\u003e" - ] + "template": ["stage=finish", "\u003c*\u003e", "tokens=\u003cNUM\u003e"] } ], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -211,21 +197,12 @@ "id": 1, "size": 58, "stage": "plan", - "template": [ - "stage=plan", - "errors=\u003cNUM\u003e", - "turns=\u003cNUM\u003e" - ] + "template": ["stage=plan", "errors=\u003cNUM\u003e", "turns=\u003cNUM\u003e"] } ], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -269,12 +246,7 @@ "clusters": null, "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -318,12 +290,7 @@ "clusters": null, "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -1448,12 +1415,7 @@ ], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -1493,4 +1455,4 @@ }, "next_id": 15 } -} \ No newline at end of file +} diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index 0772cc7d486..d5f14bf85df 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -116,6 +116,13 @@ type CapabilityProvider interface { // SupportsMaxContinuations returns true if this engine supports the max-continuations feature // When true, max-continuations > 1 enables autopilot/multi-run mode for the engine SupportsMaxContinuations() bool + + // SupportsNativeAgentFile returns true if this engine handles agent-file imports natively + // in its own execution steps (reading the file, stripping frontmatter, and prepending the + // content to the prompt at runtime). When false, the compiler is responsible for including + // the agent file content in prompt.txt during the activation job so that the engine just + // reads the standard /tmp/gh-aw/aw-prompts/prompt.txt as usual. + SupportsNativeAgentFile() bool } // WorkflowExecutor handles workflow compilation and execution @@ -260,6 +267,7 @@ type BaseEngine struct { supportsMaxTurns bool supportsMaxContinuations bool supportsWebSearch bool + supportsNativeAgentFile bool llmGatewayPort int } @@ -295,6 +303,10 @@ func (e *BaseEngine) SupportsMaxContinuations() bool { return e.supportsMaxContinuations } +func (e *BaseEngine) SupportsNativeAgentFile() bool { + return e.supportsNativeAgentFile +} + func (e *BaseEngine) getLLMGatewayPort() int { return e.llmGatewayPort } diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index bab5f7b3f80..ed0ba5a4d5d 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -29,6 +29,7 @@ func NewClaudeEngine() *ClaudeEngine { supportsMaxTurns: true, // Claude supports max-turns feature supportsMaxContinuations: false, // Claude Code does not support --max-autopilot-continues-style continuation supportsWebSearch: true, // Claude has built-in WebSearch support + supportsNativeAgentFile: false, // Claude does not support agent file natively; the compiler prepends the agent file content to prompt.txt llmGatewayPort: constants.ClaudeLLMGatewayPort, }, } @@ -166,25 +167,11 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str claudeArgs = append(claudeArgs, workflowData.EngineConfig.Args...) } - // Build the agent command - prepend custom agent file content if specified (via imports) - var promptSetup string - var promptCommand string - if workflowData.AgentFile != "" { - agentPath, err := ResolveAgentFilePath(workflowData.AgentFile) - if err != nil { - claudeLog.Printf("Error resolving agent file path: %v", err) - return BuildInvalidAgentPathStep("Execute Claude Code CLI", workflowData.AgentFile, err) - } - claudeLog.Printf("Using custom agent file: %s", workflowData.AgentFile) - // Extract markdown body from custom agent file and prepend to prompt - promptSetup = fmt.Sprintf(`# Extract markdown body from custom agent file (skip frontmatter) - AGENT_CONTENT="$(awk 'BEGIN{skip=1} /^---$/{if(skip){skip=0;next}else{skip=1;next}} !skip' %s)" - # Combine agent content with prompt - PROMPT_TEXT="$(printf '%%s\n\n%%s' "$AGENT_CONTENT" "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)")"`, agentPath) - promptCommand = `"$PROMPT_TEXT"` - } else { - promptCommand = `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"` - } + // The prompt is always read from prompt.txt, which is assembled by the compiler in the + // activation job. For engines that do not support native agent-file handling (including + // Claude), the compiler prepends the agent file content to prompt.txt so no special + // shell variable juggling is needed here. + promptCommand := `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"` // Build the command string with proper argument formatting // Determine which command to use @@ -202,8 +189,8 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str // Join command parts (excluding the prompt) with proper escaping. // The prompt command is appended raw after shellJoinArgs because it contains - // shell variable references ("$PROMPT_TEXT", "$(cat ...)") that must NOT be - // escaped — single-quoting them would prevent shell expansion at runtime. + // shell variable references ("$(cat ...)") that must NOT be escaped — + // single-quoting them would prevent shell expansion at runtime. claudeCommand := fmt.Sprintf("%s %s", shellJoinArgs(commandParts), promptCommand) // When model is not configured, use the GH_AW_MODEL_AGENT_CLAUDE fallback env var @@ -241,17 +228,6 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str npmPathSetup := GetNpmBinPathSetup() claudeCommandWithPath := fmt.Sprintf(`%s && %s`, npmPathSetup, claudeCommand) - // Build host-side path setup: create the agent step summary file so it is accessible - // inside the sandbox. Combine with any existing promptSetup (may be empty). - touchSummary := "touch " + AgentStepSummaryPath - hostSetup := touchSummary - if promptSetup != "" { - hostSetup = promptSetup + "\n" + touchSummary - } - - // Note: Claude Code CLI writes debug logs to --debug-file and JSON output to stdout - // Use tee to capture stdout (stream-json output) to the log file while also displaying on console - // The combined output (debug logs + JSON) will be in the log file for parsing command = BuildAWFCommand(AWFCommandConfig{ EngineName: "claude", EngineCommand: claudeCommandWithPath, // Command with npm PATH setup runs inside AWF @@ -259,7 +235,7 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str WorkflowData: workflowData, UsesTTY: true, // Claude Code CLI requires TTY AllowedDomains: allowedDomains, - PathSetup: hostSetup, // Runs BEFORE AWF on the host (prompt setup + summary file creation) + PathSetup: "touch " + AgentStepSummaryPath, // Runs BEFORE AWF on the host // Exclude every env var whose step-env value is a secret so the agent // cannot read raw token values via bash tools (env / printenv). ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, []string{"ANTHROPIC_API_KEY"}), @@ -270,18 +246,10 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str // Use tee to capture stdout (stream-json output) to the log file while also displaying on console // The combined output (debug logs + JSON) will be in the log file for parsing // PATH is already set correctly by actions/setup-* steps which prepend to PATH - if promptSetup != "" { - command = fmt.Sprintf(`set -o pipefail - touch %s - %s - # Execute Claude Code CLI with prompt from file - %s 2>&1 | tee -a %s`, AgentStepSummaryPath, promptSetup, claudeCommand, logFile) - } else { - command = fmt.Sprintf(`set -o pipefail + command = fmt.Sprintf(`set -o pipefail touch %s # Execute Claude Code CLI with prompt from file %s 2>&1 | tee -a %s`, AgentStepSummaryPath, claudeCommand, logFile) - } } // Build environment variables map diff --git a/pkg/workflow/claude_engine_test.go b/pkg/workflow/claude_engine_test.go index 8791d76dde1..3169cdc93bd 100644 --- a/pkg/workflow/claude_engine_test.go +++ b/pkg/workflow/claude_engine_test.go @@ -392,7 +392,9 @@ func TestClaudeEngineWithSafeOutputs(t *testing.T) { } } -// TestClaudeEngineNoDoubleEscapePrompt tests that the prompt argument is not double-escaped +// TestClaudeEngineNoDoubleEscapePrompt tests that the prompt argument is not double-escaped. +// Claude always reads the prompt from prompt.txt; agent-file content is prepended there by +// the compiler rather than being handled in the engine step. func TestClaudeEngineNoDoubleEscapePrompt(t *testing.T) { engine := NewClaudeEngine() @@ -419,8 +421,9 @@ func TestClaudeEngineNoDoubleEscapePrompt(t *testing.T) { } }) - // Test with agent file (custom prompt) - t.Run("with_agent_file", func(t *testing.T) { + // Test with agent file: Claude still reads from prompt.txt (compiler prepended the agent + // file content there); no PROMPT_TEXT shell variable should appear in the step. + t.Run("with_agent_file_uses_prompt_txt", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", EngineConfig: &EngineConfig{ @@ -432,18 +435,66 @@ func TestClaudeEngineNoDoubleEscapePrompt(t *testing.T) { steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") stepContent := strings.Join([]string(steps[0]), "\n") - // Should have single-quoted PROMPT_TEXT, not double-quoted - if strings.Contains(stepContent, `""$PROMPT_TEXT""`) { - t.Errorf("Found double-escaped PROMPT_TEXT variable (with double quotes), expected single quotes:\n%s", stepContent) + // Must still read from prompt.txt — not from a PROMPT_TEXT shell variable + if !strings.Contains(stepContent, `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`) { + t.Errorf("Expected claude to read from prompt.txt even with agent file set, got:\n%s", stepContent) } - - // Should have correctly quoted PROMPT_TEXT - if !strings.Contains(stepContent, `"$PROMPT_TEXT"`) { - t.Errorf("Expected correctly quoted PROMPT_TEXT variable, got:\n%s", stepContent) + if strings.Contains(stepContent, "PROMPT_TEXT") { + t.Errorf("Claude must not use a PROMPT_TEXT shell variable when an agent file is set; compiler handles the prepending:\n%s", stepContent) } }) } +// TestClaudeEngineDoesNotSupportNativeAgentFile verifies that the Claude engine declares +// it does not handle agent files natively, so the compiler knows to prepend the agent file +// content to prompt.txt during the activation job instead. +func TestClaudeEngineDoesNotSupportNativeAgentFile(t *testing.T) { + engine := NewClaudeEngine() + if engine.SupportsNativeAgentFile() { + t.Errorf("Claude engine should return false for SupportsNativeAgentFile(); the compiler handles agent file injection") + } +} + +// TestClaudeEngineAWFWithAgentFileReadsPromptTxt verifies that when an agent file is used +// with the firewall (AWF) enabled, the claude command reads from prompt.txt (not from a +// PROMPT_TEXT shell variable). The compiler prepends the agent file content to prompt.txt +// in the activation job. +func TestClaudeEngineAWFWithAgentFileReadsPromptTxt(t *testing.T) { + engine := NewClaudeEngine() + + agentSandbox := &AgentSandboxConfig{Type: SandboxTypeAWF} + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "claude", + }, + AgentFile: ".github/agents/test-agent.md", + SandboxConfig: &SandboxConfig{ + Agent: agentSandbox, + }, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/agent-stdio.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + // No AGENT_CONTENT or PROMPT_TEXT shell variables anywhere in the step. + if strings.Contains(stepContent, "AGENT_CONTENT") { + t.Errorf("AGENT_CONTENT must not appear in the Claude AWF step; compiler handles agent file injection:\n%s", stepContent) + } + if strings.Contains(stepContent, "PROMPT_TEXT") { + t.Errorf("PROMPT_TEXT must not appear in the Claude AWF step; compiler handles agent file injection:\n%s", stepContent) + } + + // The container command must still read from prompt.txt. + if !strings.Contains(stepContent, `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`) { + t.Errorf("Expected claude to read from prompt.txt in AWF mode, got:\n%s", stepContent) + } +} + func TestClaudeEngineSkipInstallationWithCommand(t *testing.T) { engine := NewClaudeEngine() diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 568bf0815c1..18125c0ea2e 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -40,6 +40,7 @@ func NewCodexEngine() *CodexEngine { supportsMaxTurns: false, // Codex does not support max-turns feature supportsMaxContinuations: false, // Codex does not support --max-autopilot-continues-style continuation mode supportsWebSearch: true, // Codex has built-in web-search support + supportsNativeAgentFile: true, // Codex reads the agent file and prepends it to the prompt at runtime llmGatewayPort: constants.CodexLLMGatewayPort, }, } diff --git a/pkg/workflow/compiler_yaml.go b/pkg/workflow/compiler_yaml.go index b24ea4b6a1e..aef655f9b85 100644 --- a/pkg/workflow/compiler_yaml.go +++ b/pkg/workflow/compiler_yaml.go @@ -380,6 +380,15 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, pre // - Imported markdown with inputs is still inlined (compile-time substitution required) // - Main workflow markdown body uses runtime-import to allow editing without recompilation // This ensures consistency for most imports while maintaining import inputs functionality + // + // NOTE: When an engine does not support native agent-file handling + // (SupportsNativeAgentFile() == false), the agent file content is already present in the + // prompt via the standard mechanisms below — no special Step 0 is needed: + // - Agent files WITHOUT inputs: path is in data.ImportPaths → included by Step 1b. + // - Agent files WITH inputs: content is in data.ImportedMarkdown → included by Step 1a. + // - inlined-imports mode: data.AgentFile is cleared; content is in data.ImportPaths. + // Engines that DO support native agent-file handling (e.g. Codex) continue to read the + // file themselves in GetExecutionSteps alongside the runtime-import. var userPromptChunks []string var expressionMappings []*ExpressionMapping diff --git a/pkg/workflow/engine_agent_import_test.go b/pkg/workflow/engine_agent_import_test.go index 2c850cd80b6..b4be21eaee4 100644 --- a/pkg/workflow/engine_agent_import_test.go +++ b/pkg/workflow/engine_agent_import_test.go @@ -113,7 +113,9 @@ func TestCopilotEngineWithoutAgentFlag(t *testing.T) { } } -// TestClaudeEngineWithAgentFromImports tests that claude engine prepends agent file content to prompt +// TestClaudeEngineWithAgentFromImports tests that claude engine does NOT handle agent files +// natively — agent file content is prepended to prompt.txt by the compiler in the activation +// job, so the engine step always reads the standard prompt.txt path. func TestClaudeEngineWithAgentFromImports(t *testing.T) { engine := NewClaudeEngine() workflowData := &WorkflowData{ @@ -132,19 +134,26 @@ func TestClaudeEngineWithAgentFromImports(t *testing.T) { stepContent := strings.Join([]string(steps[0]), "\n") - // Check that custom agent content extraction is present - if !strings.Contains(stepContent, `AGENT_CONTENT="$(awk`) { - t.Errorf("Expected agent content extraction in claude command, got:\n%s", stepContent) + // Claude does not handle the agent file natively — no awk or AGENT_CONTENT/PROMPT_TEXT + // variable juggling should appear in the step. + if strings.Contains(stepContent, "AGENT_CONTENT") { + t.Errorf("Claude must NOT handle agent file natively (AGENT_CONTENT found in step); the compiler handles it:\n%s", stepContent) + } + if strings.Contains(stepContent, "awk") { + t.Errorf("Claude must NOT invoke awk for agent file reading (found in step); the compiler handles it:\n%s", stepContent) + } + if strings.Contains(stepContent, "PROMPT_TEXT") { + t.Errorf("Claude must NOT use a PROMPT_TEXT shell variable (found in step); the compiler handles it:\n%s", stepContent) } - // Check that agent file path is referenced with quoted GITHUB_WORKSPACE prefix - if !strings.Contains(stepContent, `"${GITHUB_WORKSPACE}/.github/agents/test-agent.md"`) { - t.Errorf("Expected agent file path with quoted GITHUB_WORKSPACE prefix in claude command, got:\n%s", stepContent) + // The engine still reads the standard prompt.txt (which has agent content prepended by the compiler). + if !strings.Contains(stepContent, `"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`) { + t.Errorf("Expected standard prompt.txt reading in claude command, got:\n%s", stepContent) } - // Check that agent content is prepended to prompt - if !strings.Contains(stepContent, "$AGENT_CONTENT") { - t.Errorf("Expected $AGENT_CONTENT variable in claude command, got:\n%s", stepContent) + // The engine reports that it does not support native agent file handling. + if engine.SupportsNativeAgentFile() { + t.Errorf("Claude engine should return false for SupportsNativeAgentFile()") } } @@ -344,8 +353,11 @@ This is a test agent file. }) } -// TestInvalidAgentFilePathGeneratesFailingStep tests that engines emit a clearly-failing step -// (rather than silently skipping execution) when an agent file path contains shell metacharacters. +// TestInvalidAgentFilePathGeneratesFailingStep tests that engines that handle agent files +// natively emit a clearly-failing step (rather than silently skipping execution) when the +// agent file path contains shell metacharacters. +// Engines that do NOT support native agent files (e.g. Claude) rely on the compiler's +// validateAgentFile to reject malicious paths at compile time instead. func TestInvalidAgentFilePathGeneratesFailingStep(t *testing.T) { maliciousPath := `.github/agents/a";id;"b.md` @@ -373,7 +385,10 @@ func TestInvalidAgentFilePathGeneratesFailingStep(t *testing.T) { } }) - t.Run("claude_emits_failing_step_for_invalid_path", func(t *testing.T) { + // Claude does not handle agent files natively; path validation is done by the compiler + // at compile time (validateAgentFile). The engine step should proceed normally and never + // reference the agent file path directly. + t.Run("claude_ignores_agent_path_in_step_for_invalid_path", func(t *testing.T) { engine := NewClaudeEngine() workflowData := &WorkflowData{ Name: "test-workflow", @@ -382,18 +397,15 @@ func TestInvalidAgentFilePathGeneratesFailingStep(t *testing.T) { steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") if len(steps) != 1 { - t.Fatalf("Expected exactly 1 failing step, got %d", len(steps)) + t.Fatalf("Expected exactly 1 step, got %d", len(steps)) } content := strings.Join([]string(steps[0]), "\n") - if !strings.Contains(content, "exit 1") { - t.Errorf("Expected failing step with 'exit 1', got:\n%s", content) - } - if !strings.Contains(content, "Error") { - t.Errorf("Expected error message in failing step, got:\n%s", content) + // Must NOT reference the malicious path at all in the generated step + if strings.Contains(content, maliciousPath) { + t.Errorf("Claude step must not reference the agent file path directly, got:\n%s", content) } - // Must NOT invoke awk (that would mean the path was used for real execution) if strings.Contains(content, "awk") { - t.Errorf("Failing step must not invoke awk with the invalid path, got:\n%s", content) + t.Errorf("Claude step must not invoke awk for agent file reading, got:\n%s", content) } }) } @@ -466,3 +478,58 @@ func TestCheckoutWithAgentFromImports(t *testing.T) { } }) } + +// TestCompilerIncludesAgentFileViaImportPaths verifies that when a non-native engine (Claude) +// is used with an agent file, the agent file path is included in the prompt via the standard +// ImportPaths/runtime-import mechanism (Step 1b in generatePrompt), so that prompt.txt +// already contains the agent file content when the engine reads it. +func TestCompilerIncludesAgentFileViaImportPaths(t *testing.T) { + agentFilePath := ".github/agents/my-agent.md" + + tmpDir := t.TempDir() + workflowFile := filepath.Join(tmpDir, ".github", "workflows", "test.md") + if err := os.MkdirAll(filepath.Dir(workflowFile), 0o755); err != nil { + t.Fatalf("Failed to create workflow directory: %v", err) + } + if err := os.WriteFile(workflowFile, []byte("# Do the thing\n"), 0o644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Simulate what the orchestrator populates: the agent file is in ImportPaths (no inputs). + workflowData := &WorkflowData{ + Name: "test-workflow", + AI: "claude", + EngineConfig: &EngineConfig{ + ID: "claude", + }, + AgentFile: agentFilePath, + // ImportPaths mirrors what import_bfs.go populates for agent files without inputs. + ImportPaths: []string{agentFilePath}, + } + + compiler := NewCompiler() + compiler.markdownPath = workflowFile + + var buf strings.Builder + compiler.generatePrompt(&buf, workflowData, false, nil) + generated := buf.String() + + // The runtime-import macro for the agent file must appear in the generated YAML (exactly once). + agentImportMacro := "{{#runtime-import " + agentFilePath + "}}" + count := strings.Count(generated, agentImportMacro) + if count == 0 { + t.Errorf("Expected runtime-import macro %q in generated prompt YAML, got:\n%s", agentImportMacro, generated) + } else if count > 1 { + t.Errorf("Expected runtime-import macro %q exactly once, but found %d occurrences:\n%s", agentImportMacro, count, generated) + } + + // The agent file import must appear before the main workflow markdown import. + mainWorkflowMacro := "{{#runtime-import .github/workflows/test.md}}" + agentIdx := strings.Index(generated, agentImportMacro) + mainIdx := strings.Index(generated, mainWorkflowMacro) + if mainIdx == -1 { + t.Errorf("Expected main workflow runtime-import macro %q in generated prompt YAML, got:\n%s", mainWorkflowMacro, generated) + } else if agentIdx > mainIdx { + t.Errorf("Agent file runtime-import macro must appear before main workflow macro in prompt:\n%s", generated) + } +}