From 31350d5e0541884af69324b7283543394c37512e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 12 Jun 2026 22:17:41 +0000
Subject: [PATCH] chore: sync actions from gh-aw@v0.79.8
---
setup/js/ai_credits_context.cjs | 14 +-
setup/js/apply_samples.cjs | 1 +
setup/js/artifact_client.cjs | 6 +
.../js/check_daily_aic_workflow_guardrail.cjs | 19 +-
setup/js/convert_gateway_config_copilot.cjs | 41 +-
setup/js/emit_outcome_spans.cjs | 10 +-
setup/js/handle_agent_failure.cjs | 7 +-
setup/js/log_parser_bootstrap.cjs | 4 +-
setup/js/log_parser_format.cjs | 27 +-
setup/js/log_parser_shared.cjs | 387 ++++++++++++++++++
setup/js/otlp.cjs | 2 +-
setup/js/parse_antigravity_log.cjs | 6 +-
setup/js/parse_claude_log.cjs | 7 +-
setup/js/parse_codex_log.cjs | 6 +-
setup/js/parse_copilot_log.cjs | 248 ++---------
setup/js/parse_gemini_log.cjs | 7 +-
setup/js/parse_pi_log.cjs | 7 +-
setup/js/safe_output_type_validator.cjs | 9 +
setup/js/safe_outputs_tools.json | 6 +-
setup/js/send_otlp_span.cjs | 5 +-
setup/js/set_issue_field.cjs | 96 ++---
setup/js/start_mcp_gateway.cjs | 84 +++-
setup/js/validate_context_variables.cjs | 83 ++--
setup/post.js | 75 ++++
setup/sh/convert_gateway_config_copilot.sh | 17 +-
setup/sh/start_mcp_gateway.sh | 22 +-
26 files changed, 798 insertions(+), 398 deletions(-)
diff --git a/setup/js/ai_credits_context.cjs b/setup/js/ai_credits_context.cjs
index 14f9fea0..54253052 100644
--- a/setup/js/ai_credits_context.cjs
+++ b/setup/js/ai_credits_context.cjs
@@ -319,6 +319,8 @@ function resolveAICreditsFailureState({ logProvenance = true } = {}) {
const { aiCredits: auditAICredits, maxAICredits: auditMaxAICredits, rateLimitError: auditRateLimitError, maxAICreditsExceeded: auditMaxAICreditsExceeded } = parseAuditLogCombined();
const envAICredits = parsePositiveNumberString(process.env.GH_AW_AIC);
const envMaxAICredits = parsePositiveNumberString(process.env.GH_AW_MAX_AI_CREDITS);
+ const envRateLimitSignal = process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true";
+ const envRateLimitSignalHasEvidence = envRateLimitSignal && !!(auditAICredits || stdioSignals.aiCredits || envAICredits);
// Log provenance so failing issues can be diagnosed when credit data is missing.
if (logProvenance) {
@@ -342,13 +344,21 @@ function resolveAICreditsFailureState({ logProvenance = true } = {}) {
console.log(`[ai-credits] maxAICredits source=none GH_AW_MAX_AI_CREDITS=${process.env.GH_AW_MAX_AI_CREDITS || "(unset)"}`);
}
- const rawRateLimitSignalSource = auditRateLimitError ? "audit_log" : stdioSignals.rateLimitError ? "agent_stdio" : process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true" ? "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)" : "none";
+ const rawRateLimitSignalSource = auditRateLimitError
+ ? "audit_log"
+ : stdioSignals.rateLimitError
+ ? "agent_stdio"
+ : envRateLimitSignalHasEvidence
+ ? "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)"
+ : envRateLimitSignal
+ ? "env_ignored_no_ai_credits"
+ : "none";
console.log(`[ai-credits] rateLimitSignal source=${rawRateLimitSignalSource}`);
}
const aiCredits = auditAICredits || stdioSignals.aiCredits || envAICredits || "";
const maxAICredits = auditMaxAICredits || stdioSignals.maxAICredits || envMaxAICredits || "";
- const rawAICreditsRateLimitError = auditRateLimitError || stdioSignals.rateLimitError || process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true";
+ const rawAICreditsRateLimitError = auditRateLimitError || stdioSignals.rateLimitError || envRateLimitSignalHasEvidence;
const aiCreditsRateLimitError = shouldReportAICreditsRateLimitError(rawAICreditsRateLimitError, aiCredits, maxAICredits);
return { aiCredits, maxAICredits, aiCreditsRateLimitError, maxAICreditsExceeded: auditMaxAICreditsExceeded || stdioSignals.maxAICreditsExceeded };
}
diff --git a/setup/js/apply_samples.cjs b/setup/js/apply_samples.cjs
index 68274141..c632661b 100644
--- a/setup/js/apply_samples.cjs
+++ b/setup/js/apply_samples.cjs
@@ -1,6 +1,7 @@
#!/usr/bin/env node
// @ts-check
///
+// @safe-outputs-exempt SEC-005: target repo is used only for read-only PR head-ref lookups during deterministic sample replay; never derived from agent safe-output content and never used for a cross-repo write.
// apply_samples.cjs
//
diff --git a/setup/js/artifact_client.cjs b/setup/js/artifact_client.cjs
index 1b463334..40b69740 100644
--- a/setup/js/artifact_client.cjs
+++ b/setup/js/artifact_client.cjs
@@ -1,6 +1,12 @@
// @ts-check
///
+/**
+ * @safe-outputs-exempt SEC-004: "body" references are HTTP transport payloads
+ * (Twirp RPC request JSON bodies and artifact upload stream bodies), not
+ * user-authored issue/PR/comment bodies.
+ */
+
const crypto = require("crypto");
const fs = require("fs");
const os = require("os");
diff --git a/setup/js/check_daily_aic_workflow_guardrail.cjs b/setup/js/check_daily_aic_workflow_guardrail.cjs
index 6896ffab..905f42c0 100644
--- a/setup/js/check_daily_aic_workflow_guardrail.cjs
+++ b/setup/js/check_daily_aic_workflow_guardrail.cjs
@@ -382,6 +382,11 @@ async function main() {
const candidateRuns = [];
let page = 1;
let truncatedByRateLimit = false;
+ // listWorkflowRuns returns runs in descending creation order (newest first).
+ // The first run whose created_at falls before the cutoff means all remaining
+ // runs on this page and every subsequent page are also outside the window, so
+ // we can stop paginating immediately rather than exhausting the page budget.
+ let reachedCutoff = false;
while (page <= MAX_WORKFLOW_RUN_PAGES) {
logDailyGuardrail("Querying completed workflow runs", {
workflowId: currentRun.data.workflow_id,
@@ -401,7 +406,8 @@ async function main() {
logDailyGuardrail("Received workflow runs page", {
page,
runCount: runs.length,
- runIds: runs.map(run => run?.id).filter(Boolean),
+ firstRunId: runs[0]?.id ?? null,
+ lastRunId: runs[runs.length - 1]?.id ?? null,
});
if (runs.length === 0) {
break;
@@ -412,7 +418,10 @@ async function main() {
}
const createdAtMs = Date.parse(run.created_at || "");
if (!Number.isFinite(createdAtMs) || createdAtMs < cutoffMs) {
- continue;
+ // Runs are newest-first; any run older than the cutoff means all
+ // remaining runs (and pages) are also outside the 24h window.
+ reachedCutoff = true;
+ break;
}
candidateRuns.push(run);
if (candidateRuns.length >= maxInspectableRuns) {
@@ -420,7 +429,7 @@ async function main() {
break;
}
}
- if (candidateRuns.length >= maxInspectableRuns || runs.length < 100) {
+ if (reachedCutoff || candidateRuns.length >= maxInspectableRuns || runs.length < 100) {
break;
}
page += 1;
@@ -437,10 +446,6 @@ async function main() {
/** @type {Array<{id:number, html_url:string, created_at:string, conclusion:string, aic:number}>} */
const countedRuns = [];
for (const run of candidateRuns) {
- if (countedRuns.length >= maxInspectableRuns) {
- truncatedByRateLimit = true;
- break;
- }
try {
const runAIC = await module.exports.getRunAIC(artifactClient, run.id, token, owner, repo);
if (runAIC <= 0) {
diff --git a/setup/js/convert_gateway_config_copilot.cjs b/setup/js/convert_gateway_config_copilot.cjs
index c574b73f..bda12b20 100644
--- a/setup/js/convert_gateway_config_copilot.cjs
+++ b/setup/js/convert_gateway_config_copilot.cjs
@@ -10,20 +10,43 @@ require("./shim.cjs");
* Converts the MCP gateway's standard HTTP-based configuration to the format
* expected by GitHub Copilot CLI. Reads the gateway output JSON, filters out
* CLI-mounted servers, adds tools:["*"] if missing, rewrites URLs to use the
- * correct domain, and writes the result to /home/runner/.copilot/mcp-config.json.
+ * correct domain, and writes the result to $HOME/.copilot/mcp-config.json
+ * (typically /home/runner/.copilot/mcp-config.json on GitHub-hosted runners,
+ * but may differ on self-hosted or containerized runners where HOME varies).
*
* Required environment variables:
* - MCP_GATEWAY_OUTPUT: Path to gateway output configuration file
* - MCP_GATEWAY_DOMAIN: Domain for MCP server URLs (e.g., host.docker.internal)
* - MCP_GATEWAY_PORT: Port for MCP gateway (e.g., 80)
+ * - HOME: User home directory (standard POSIX env var inherited by the runner)
*
* Optional:
* - GH_AW_MCP_CLI_SERVERS: JSON array of server names to exclude from agent config
*/
+const path = require("path");
const { rewriteUrl, loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs");
-const OUTPUT_PATH = "/home/runner/.copilot/mcp-config.json";
+/**
+ * Resolves the Copilot CLI MCP config output path from the runtime $HOME.
+ * The Copilot CLI uses ~/.copilot, which is /home/runner/.copilot on standard
+ * GitHub-hosted runners (HOME=/home/runner) but may differ on self-hosted or
+ * containerized runners. HOME is a standard POSIX environment variable inherited
+ * from the runner's parent process and passed through to shell steps; other
+ * generators (copilot_mcp.go, copilot_engine_execution.go) rely on it the same way.
+ *
+ * Exported for testability; throws Error rather than exiting so tests can
+ * exercise the missing-HOME branch.
+ *
+ * @returns {string}
+ */
+function resolveCopilotConfigOutputPath() {
+ const home = process.env.HOME;
+ if (!home) {
+ throw new Error("HOME environment variable is not set; cannot locate Copilot CLI config directory");
+ }
+ return path.join(home, ".copilot", "mcp-config.json");
+}
/**
* @param {Record} entry
@@ -44,6 +67,14 @@ function transformCopilotEntry(entry, urlPrefix) {
}
function main() {
+ let outputPath;
+ try {
+ outputPath = resolveCopilotConfigOutputPath();
+ } catch (err) {
+ core.error(`ERROR: ${err.message}`);
+ process.exit(1);
+ }
+
const { gatewayOutput, domain, port, urlPrefix, cliServers, servers } = loadGatewayContext();
core.info("Converting gateway configuration to Copilot format...");
@@ -58,9 +89,9 @@ function main() {
// Write with owner-only permissions (0o600) to protect the gateway bearer token.
// An attacker who reads mcp-config.json could bypass --allowed-tools by issuing
// raw JSON-RPC calls directly to the gateway.
- writeSecureOutput(OUTPUT_PATH, output);
+ writeSecureOutput(outputPath, output);
- core.info(`Copilot configuration written to ${OUTPUT_PATH}`);
+ core.info(`Copilot configuration written to ${outputPath}`);
core.info("");
core.info("Converted configuration:");
core.info(output);
@@ -70,4 +101,4 @@ if (require.main === module) {
main();
}
-module.exports = { rewriteUrl, transformCopilotEntry, main };
+module.exports = { rewriteUrl, transformCopilotEntry, resolveCopilotConfigOutputPath, main };
diff --git a/setup/js/emit_outcome_spans.cjs b/setup/js/emit_outcome_spans.cjs
index 4b2ed4a7..9572a00e 100644
--- a/setup/js/emit_outcome_spans.cjs
+++ b/setup/js/emit_outcome_spans.cjs
@@ -86,12 +86,12 @@ async function main() {
console.log("[outcome-otel] No OTLP endpoints configured, writing JSONL mirror only");
}
- // Read aw_info.json first: GH_AW_INFO_VERSION and GH_AW_INFO_STAGED are only
- // present during setup, while aw_info.json is the authoritative runtime
- // source for later github-script steps. Prefer agent_version when available
- // to match the other OTEL helpers' service/scope version attribution.
+ // Read aw_info.json first: GH_AW_INFO_* env vars are only present during setup,
+ // while aw_info.json is the authoritative runtime source for later
+ // github-script steps. Prefer cli_version for CLI-named version dimensions,
+ // and only fall back to engine version fields when CLI version is unavailable.
const staged = awInfo.staged === true || process.env.GH_AW_INFO_STAGED === "true";
- const scopeVersion = awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || "unknown";
+ const scopeVersion = (typeof awInfo.cli_version === "string" ? awInfo.cli_version : "") || process.env.GH_AW_INFO_CLI_VERSION || awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || "unknown";
const traceId = (process.env.GITHUB_AW_OTEL_TRACE_ID || "").trim().toLowerCase() || generateTraceId();
const parentSpanId = (process.env.GITHUB_AW_OTEL_PARENT_SPAN_ID || "").trim().toLowerCase() || "";
const summarySpanId = generateSpanId();
diff --git a/setup/js/handle_agent_failure.cjs b/setup/js/handle_agent_failure.cjs
index e9eb3dd0..6728a501 100644
--- a/setup/js/handle_agent_failure.cjs
+++ b/setup/js/handle_agent_failure.cjs
@@ -11,7 +11,7 @@ const { MAX_SUB_ISSUES, getSubIssueCount } = require("./sub_issue_helpers.cjs");
const { formatMissingData, formatMissingTools } = require("./missing_info_formatter.cjs");
const { generateHistoryUrl } = require("./generate_history_link.cjs");
const { AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs");
-const { resolveFirewallAuditLogPath, resolveAICreditsFailureState, parseMaxAICreditsFromAuditLog, parseAICreditsErrorInfoFromAuditLog } = require("./ai_credits_context.cjs");
+const { resolveFirewallAuditLogPath, resolveAICreditsFailureState, parseMaxAICreditsFromAuditLog, parseAICreditsErrorInfoFromAuditLog, parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context.cjs");
const { formatAICCredits } = require("./daily_aic_workflow_helpers.cjs");
const { formatAIC } = require("./model_costs.cjs");
const { parseTokenUsageJsonl, generateTokenUsageSummary } = require("./parse_mcp_gateway_log.cjs");
@@ -2382,7 +2382,9 @@ async function main() {
const mcpPolicyError = process.env.GH_AW_MCP_POLICY_ERROR === "true";
const agenticEngineTimeout = process.env.GH_AW_AGENTIC_ENGINE_TIMEOUT === "true";
const modelNotSupportedError = process.env.GH_AW_MODEL_NOT_SUPPORTED_ERROR === "true";
- const unknownModelAICredits = process.env.GH_AW_UNKNOWN_MODEL_AI_CREDITS === "true";
+ const unknownModelAICreditsFromOutput = process.env.GH_AW_UNKNOWN_MODEL_AI_CREDITS === "true";
+ const unknownModelAICreditsFromAudit = parseUnknownModelAICreditsFromAuditLog();
+ const unknownModelAICredits = unknownModelAICreditsFromAudit || (unknownModelAICreditsFromOutput && agentConclusion === "failure");
const pushRepoMemoryResult = process.env.GH_AW_PUSH_REPO_MEMORY_RESULT || "";
const reportFailureAsIssue = process.env.GH_AW_FAILURE_REPORT_AS_ISSUE !== "false"; // Default to true
// Feature flags: control whether missing_tool/missing_data signals trigger agent failure handling.
@@ -2450,6 +2452,7 @@ async function main() {
core.info(`Agentic engine timeout: ${agenticEngineTimeout}`);
core.info(`Model not supported error: ${modelNotSupportedError}`);
core.info(`Unknown model AI credits error: ${unknownModelAICredits}`);
+ core.info(`Unknown model AI credits sources (audit/output): ${unknownModelAICreditsFromAudit}/${unknownModelAICreditsFromOutput}`);
core.info(`Push repo-memory result: ${pushRepoMemoryResult}`);
core.info(`App token minting failed (safe_outputs/conclusion/activation): ${safeOutputsAppTokenMintingFailed}/${conclusionAppTokenMintingFailed}/${activationAppTokenMintingFailed}`);
core.info(`Lockdown check failed: ${hasLockdownCheckFailed}`);
diff --git a/setup/js/log_parser_bootstrap.cjs b/setup/js/log_parser_bootstrap.cjs
index 99a06b93..8482b9d5 100644
--- a/setup/js/log_parser_bootstrap.cjs
+++ b/setup/js/log_parser_bootstrap.cjs
@@ -164,8 +164,8 @@ async function runLogParser(options) {
// Generate lightweight plain text summary for core.info and Copilot CLI style for step summary
if (logEntries && Array.isArray(logEntries) && logEntries.length > 0) {
// Extract model from init entry if available
- const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init");
- const model = initEntry?.model || null;
+ const initEntry = logEntries.find(entry => (entry.type === "system" && entry.subtype === "init") || entry.type === "session.init");
+ const model = initEntry?.model || initEntry?.data?.model || null;
const plainTextSummary = generatePlainTextSummary(logEntries, {
model,
diff --git a/setup/js/log_parser_format.cjs b/setup/js/log_parser_format.cjs
index 4c9a5cd3..43fa01dc 100644
--- a/setup/js/log_parser_format.cjs
+++ b/setup/js/log_parser_format.cjs
@@ -15,6 +15,8 @@
* @property {(text: string) => number} estimateTokens
* @property {(ms: number) => string} formatDuration
* @property {(text: string) => string} unfenceMarkdown
+ * @property {(entries: Array) => boolean} isCopilotEventLogEntries
+ * @property {(entries: Array) => Array} convertCopilotEventsToLegacyLogEntries
* @property {number} MAX_AGENT_TEXT_LENGTH
* @property {string} SIZE_LIMIT_WARNING
*/
@@ -48,12 +50,21 @@ function createLogParserFormatters(deps) {
estimateTokens,
formatDuration,
unfenceMarkdown,
+ isCopilotEventLogEntries,
+ convertCopilotEventsToLegacyLogEntries,
MAX_AGENT_TEXT_LENGTH,
SIZE_LIMIT_WARNING,
} = deps;
const INTERNAL_TOOLS = ["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"];
+ function normalizeEntriesForRendering(logEntries) {
+ if (isCopilotEventLogEntries(logEntries)) {
+ return convertCopilotEventsToLegacyLogEntries(logEntries);
+ }
+ return logEntries;
+ }
+
/**
* Generates markdown summary from conversation log entries
* This is the core shared logic between Claude and Copilot log parsers
@@ -70,8 +81,9 @@ function createLogParserFormatters(deps) {
*/
function generateConversationMarkdown(logEntries, options) {
const { formatToolCallback, formatInitCallback, summaryTracker } = options;
+ const renderEntries = normalizeEntriesForRendering(logEntries);
- const toolUsePairs = collectToolUsePairs(logEntries);
+ const toolUsePairs = collectToolUsePairs(renderEntries);
let markdown = "";
let sizeLimitReached = false;
@@ -85,7 +97,7 @@ function createLogParserFormatters(deps) {
return true;
}
- const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init");
+ const initEntry = renderEntries.find(entry => entry.type === "system" && entry.subtype === "init");
if (initEntry && formatInitCallback) {
if (!addContent("## 🚀 Initialization\n\n")) {
@@ -110,7 +122,7 @@ function createLogParserFormatters(deps) {
return { markdown, commandSummary: [], sizeLimitReached };
}
- for (const entry of logEntries) {
+ for (const entry of renderEntries) {
if (sizeLimitReached) break;
if (entry.type === "assistant" && entry.message?.content) {
@@ -158,7 +170,7 @@ function createLogParserFormatters(deps) {
const commandSummary = [];
- for (const entry of logEntries) {
+ for (const entry of renderEntries) {
if (entry.type === "assistant" && entry.message?.content) {
for (const content of entry.message.content) {
if (content.type === "tool_use") {
@@ -522,8 +534,9 @@ function createLogParserFormatters(deps) {
}
function generateSummaryLines(logEntries) {
+ const renderEntries = normalizeEntriesForRendering(logEntries);
const lines = [];
- const toolUsePairs = collectToolUsePairs(logEntries);
+ const toolUsePairs = collectToolUsePairs(renderEntries);
const state = {
conversationLineCount: 0,
@@ -532,7 +545,7 @@ function createLogParserFormatters(deps) {
traceEventCount: 0,
};
- for (const entry of logEntries) {
+ for (const entry of renderEntries) {
if (state.conversationLineCount >= state.maxConversationLines) {
state.conversationTruncated = true;
break;
@@ -569,7 +582,7 @@ function createLogParserFormatters(deps) {
lines.push("");
}
- appendStatistics(lines, logEntries, toolUsePairs);
+ appendStatistics(lines, renderEntries, toolUsePairs);
return lines;
}
diff --git a/setup/js/log_parser_shared.cjs b/setup/js/log_parser_shared.cjs
index 6fa5f8ee..e0a5a06b 100644
--- a/setup/js/log_parser_shared.cjs
+++ b/setup/js/log_parser_shared.cjs
@@ -610,6 +610,388 @@ function parseLogEntries(logContent) {
return logEntries;
}
+/**
+ * Detects whether entries are in Copilot event log format.
+ * @param {Array} logEntries
+ * @returns {boolean}
+ */
+function isCopilotEventLogEntries(logEntries) {
+ if (!Array.isArray(logEntries) || logEntries.length === 0) {
+ return false;
+ }
+
+ const eventTypePrefixes = ["user.", "assistant.", "tool.", "session."];
+ let eventLikeCount = 0;
+
+ for (const entry of logEntries) {
+ if (!entry || typeof entry !== "object" || typeof entry.type !== "string") continue;
+ if (entry.type === "assistant" || entry.type === "user" || entry.type === "system" || entry.type === "result") {
+ return false;
+ }
+ if (eventTypePrefixes.some(prefix => entry.type.startsWith(prefix))) {
+ eventLikeCount++;
+ }
+ }
+
+ return eventLikeCount > 0;
+}
+
+/**
+ * Converts legacy trace entries to Copilot event log format.
+ * @param {Array} logEntries
+ * @param {{sourceEngine?: string}} [options]
+ * @returns {Array}
+ */
+function convertLegacyLogEntriesToCopilotEvents(logEntries, options = {}) {
+ if (!Array.isArray(logEntries) || logEntries.length === 0) {
+ return [];
+ }
+ if (isCopilotEventLogEntries(logEntries)) {
+ return logEntries;
+ }
+
+ const { sourceEngine = "unknown" } = options;
+ /** @type {Array} */
+ const events = [];
+ const toolUsesById = new Map();
+
+ for (const entry of logEntries) {
+ if (!entry || typeof entry !== "object") continue;
+
+ if (entry.type === "system" && entry.subtype === "init") {
+ events.push({
+ type: "session.init",
+ data: {
+ sourceEngine,
+ model: entry.model,
+ sessionId: entry.session_id,
+ cwd: entry.cwd,
+ tools: Array.isArray(entry.tools) ? entry.tools : [],
+ mcpServers: Array.isArray(entry.mcp_servers) ? entry.mcp_servers : [],
+ slashCommands: Array.isArray(entry.slash_commands) ? entry.slash_commands : [],
+ modelInfo: entry.model_info,
+ },
+ });
+ continue;
+ }
+
+ if (entry.type === "system" && entry.subtype && entry.subtype !== "init") {
+ if (entry.message?.content && Array.isArray(entry.message.content)) {
+ for (const content of entry.message.content) {
+ if (content?.type === "text" && typeof content.text === "string" && content.text.trim()) {
+ events.push({
+ type: "assistant.message",
+ data: { content: content.text },
+ });
+ }
+ }
+ } else if (typeof entry.message === "string" && entry.message.trim()) {
+ events.push({
+ type: "assistant.message",
+ data: { content: entry.message },
+ });
+ }
+ continue;
+ }
+
+ if (entry.type === "assistant" && entry.message?.content && Array.isArray(entry.message.content)) {
+ for (const content of entry.message.content) {
+ if (!content || typeof content !== "object") continue;
+
+ if (content.type === "text" && typeof content.text === "string" && content.text.trim()) {
+ events.push({
+ type: "assistant.message",
+ data: { content: content.text },
+ });
+ } else if (content.type === "thinking" && typeof content.thinking === "string" && content.thinking.trim()) {
+ events.push({
+ type: "assistant.reasoning",
+ data: { content: content.thinking },
+ });
+ } else if (content.type === "tool_use") {
+ const toolCallId = typeof content.id === "string" && content.id.trim() ? content.id : `tool_${events.length + 1}`;
+ toolUsesById.set(toolCallId, content);
+ events.push({
+ type: "tool.execution_start",
+ data: {
+ toolCallId,
+ toolName: content.name,
+ input: content.input || {},
+ },
+ });
+ }
+ }
+ continue;
+ }
+
+ if (entry.type === "user" && entry.message?.content && Array.isArray(entry.message.content)) {
+ for (const content of entry.message.content) {
+ if (!content || content.type !== "tool_result") continue;
+ const toolCallId = typeof content.tool_use_id === "string" && content.tool_use_id.trim() ? content.tool_use_id : `tool_${events.length + 1}`;
+ const toolUse = toolUsesById.get(toolCallId);
+ events.push({
+ type: "tool.execution_complete",
+ data: {
+ toolCallId,
+ toolName: toolUse?.name,
+ success: content.is_error !== true,
+ output: content.content,
+ durationMs: content.duration_ms,
+ },
+ });
+ }
+ continue;
+ }
+
+ if (entry.type === "result") {
+ events.push({
+ type: "session.result",
+ data: {
+ numTurns: entry.num_turns,
+ durationMs: entry.duration_ms,
+ totalCostUsd: entry.total_cost_usd,
+ usage: entry.usage,
+ errors: entry.errors,
+ permissionDenials: entry.permission_denials,
+ },
+ });
+ }
+ }
+
+ return events;
+}
+
+/**
+ * Converts Copilot event log entries to legacy trace entries used by renderers.
+ * @param {Array} logEntries
+ * @returns {Array}
+ */
+function convertCopilotEventsToLegacyLogEntries(logEntries) {
+ if (!Array.isArray(logEntries) || logEntries.length === 0) {
+ return [];
+ }
+ if (!isCopilotEventLogEntries(logEntries)) {
+ return logEntries;
+ }
+
+ /** @type {Array} */
+ const normalizedEntries = [];
+ const pendingByToolCallId = new Map();
+ const pendingIdsByToolName = new Map();
+ let toolCounter = 0;
+ let turnCount = 0;
+ let assistantMessageCount = 0;
+
+ const addPendingId = (toolName, toolId) => {
+ const existing = pendingIdsByToolName.get(toolName);
+ if (existing) {
+ existing.push(toolId);
+ return;
+ }
+ pendingIdsByToolName.set(toolName, [toolId]);
+ };
+
+ const shiftPendingId = toolName => {
+ const existing = pendingIdsByToolName.get(toolName);
+ if (!existing || existing.length === 0) return null;
+ const toolId = existing.shift();
+ if (existing.length === 0) {
+ pendingIdsByToolName.delete(toolName);
+ }
+ return toolId || null;
+ };
+
+ const removePendingId = (toolName, toolId) => {
+ const existing = pendingIdsByToolName.get(toolName);
+ if (!existing || existing.length === 0) return;
+ const idx = existing.indexOf(toolId);
+ if (idx === -1) return;
+ existing.splice(idx, 1);
+ if (existing.length === 0) {
+ pendingIdsByToolName.delete(toolName);
+ }
+ };
+
+ const normalizeToolName = (rawToolName, mcpServerName) => {
+ const toolName = typeof rawToolName === "string" && rawToolName.trim() ? rawToolName.trim() : "unknown";
+ if (toolName.startsWith("mcp__")) {
+ return toolName;
+ }
+ const serverName = typeof mcpServerName === "string" ? mcpServerName.trim() : "";
+ if (!serverName) {
+ return toolName;
+ }
+ return `mcp__${serverName}__${toolName}`;
+ };
+
+ const readString = (...values) => {
+ for (const value of values) {
+ if (typeof value === "string") return value;
+ }
+ return "";
+ };
+
+ for (const entry of logEntries) {
+ if (!entry || typeof entry !== "object") continue;
+ const data = entry.data && typeof entry.data === "object" ? entry.data : {};
+
+ switch (entry.type) {
+ case "session.init":
+ normalizedEntries.push({
+ type: "system",
+ subtype: "init",
+ model: data.model,
+ session_id: data.sessionId,
+ cwd: data.cwd,
+ tools: Array.isArray(data.tools) ? data.tools : [],
+ mcp_servers: Array.isArray(data.mcpServers) ? data.mcpServers : [],
+ slash_commands: Array.isArray(data.slashCommands) ? data.slashCommands : [],
+ model_info: data.modelInfo,
+ });
+ break;
+
+ case "user.message":
+ turnCount++;
+ break;
+
+ case "assistant.message": {
+ const text = readString(data.content, data.message);
+ if (!text.trim()) break;
+ assistantMessageCount++;
+ normalizedEntries.push({
+ type: "assistant",
+ message: {
+ content: [{ type: "text", text }],
+ },
+ });
+ break;
+ }
+
+ case "assistant.reasoning":
+ case "reasoning": {
+ const text = typeof data.content === "string" ? data.content : "";
+ if (!text.trim()) break;
+ normalizedEntries.push({
+ type: "assistant",
+ message: {
+ content: [{ type: "thinking", thinking: text }],
+ },
+ });
+ break;
+ }
+
+ case "tool.execution_start": {
+ const toolName = normalizeToolName(data.toolName, data.mcpServerName);
+ const toolCallId = typeof data.toolCallId === "string" && data.toolCallId.trim() ? data.toolCallId : null;
+ const resolvedToolId = toolCallId || `sdk_tool_${++toolCounter}`;
+ if (toolCallId) {
+ pendingByToolCallId.set(toolCallId, resolvedToolId);
+ }
+ addPendingId(toolName, resolvedToolId);
+ normalizedEntries.push({
+ type: "assistant",
+ message: {
+ content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: data.input || data.parameters || {} }],
+ },
+ });
+ break;
+ }
+
+ case "tool.execution_complete": {
+ const toolName = normalizeToolName(data.toolName, data.mcpServerName);
+ const toolCallId = typeof data.toolCallId === "string" && data.toolCallId.trim() ? data.toolCallId : null;
+ let resolvedToolId = null;
+
+ if (toolCallId && pendingByToolCallId.has(toolCallId)) {
+ resolvedToolId = pendingByToolCallId.get(toolCallId);
+ pendingByToolCallId.delete(toolCallId);
+ if (resolvedToolId) {
+ removePendingId(toolName, resolvedToolId);
+ }
+ }
+ if (!resolvedToolId) {
+ resolvedToolId = shiftPendingId(toolName);
+ }
+ if (!resolvedToolId) {
+ resolvedToolId = `sdk_tool_${++toolCounter}`;
+ normalizedEntries.push({
+ type: "assistant",
+ message: {
+ content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: data.input || data.parameters || {} }],
+ },
+ });
+ }
+
+ const success = typeof data.success === "boolean" ? data.success : !data.error;
+ let output = "";
+ if (typeof data.output === "string") {
+ output = data.output;
+ } else if (typeof data.result === "string") {
+ output = data.result;
+ } else if (data.error) {
+ output = String(data.error);
+ } else if (success) {
+ output = "success";
+ } else {
+ output = "Tool execution failed";
+ }
+
+ normalizedEntries.push({
+ type: "user",
+ message: {
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: resolvedToolId,
+ content: output,
+ is_error: !success,
+ duration_ms: typeof data.durationMs === "number" ? data.durationMs : undefined,
+ },
+ ],
+ },
+ });
+ break;
+ }
+
+ case "session.result": {
+ const usage = data.usage && typeof data.usage === "object" ? data.usage : {};
+ normalizedEntries.push({
+ type: "result",
+ num_turns: typeof data.numTurns === "number" ? data.numTurns : undefined,
+ duration_ms: typeof data.durationMs === "number" ? data.durationMs : undefined,
+ total_cost_usd: typeof data.totalCostUsd === "number" ? data.totalCostUsd : undefined,
+ usage: {
+ input_tokens: usage.input_tokens ?? usage.inputTokens,
+ output_tokens: usage.output_tokens ?? usage.outputTokens,
+ cache_creation_input_tokens: usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens,
+ cache_read_input_tokens: usage.cache_read_input_tokens ?? usage.cacheReadInputTokens,
+ },
+ errors: Array.isArray(data.errors) ? data.errors : undefined,
+ permission_denials: Array.isArray(data.permissionDenials) ? data.permissionDenials : undefined,
+ });
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ if (normalizedEntries.length === 0) {
+ return [];
+ }
+
+ const hasResult = normalizedEntries.some(entry => entry.type === "result");
+ if (!hasResult) {
+ normalizedEntries.push({
+ type: "result",
+ num_turns: turnCount > 0 ? turnCount : assistantMessageCount,
+ });
+ }
+
+ return normalizedEntries;
+}
+
const { generateConversationMarkdown, formatToolUse, generatePlainTextSummary, generateCopilotCliStyleSummary } = createLogParserFormatters({
formatBashCommand,
formatMcpName,
@@ -621,6 +1003,8 @@ const { generateConversationMarkdown, formatToolUse, generatePlainTextSummary, g
estimateTokens,
formatDuration,
unfenceMarkdown,
+ isCopilotEventLogEntries,
+ convertCopilotEventsToLegacyLogEntries,
MAX_AGENT_TEXT_LENGTH,
SIZE_LIMIT_WARNING,
});
@@ -963,6 +1347,9 @@ module.exports = {
formatToolDisplayName,
formatInitializationSummary,
formatToolUse,
+ isCopilotEventLogEntries,
+ convertLegacyLogEntriesToCopilotEvents,
+ convertCopilotEventsToLegacyLogEntries,
parseLogEntries,
formatToolCallAsDetails,
formatResultPreview,
diff --git a/setup/js/otlp.cjs b/setup/js/otlp.cjs
index 1b415b19..05eadd85 100644
--- a/setup/js/otlp.cjs
+++ b/setup/js/otlp.cjs
@@ -105,7 +105,7 @@ async function logSpan(toolName, attributes = {}, options = {}) {
// source (written by generate_aw_info.cjs and read by conclusion spans).
const awInfo = readJSONIfExists("/tmp/gh-aw/aw_info.json") || {};
const staged = awInfo.staged === true || process.env.GH_AW_INFO_STAGED === "true";
- const scopeVersion = awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || "unknown";
+ const scopeVersion = (typeof awInfo.cli_version === "string" ? awInfo.cli_version : "") || process.env.GH_AW_INFO_CLI_VERSION || awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || "unknown";
const awfVersion = (typeof awInfo.awf_version === "string" ? awInfo.awf_version : "") || process.env.GH_AW_INFO_AWF_VERSION || "";
const awmgVersion = (typeof awInfo.awmg_version === "string" ? awInfo.awmg_version : "") || process.env.GH_AW_INFO_AWMG_VERSION || "";
const serviceName = process.env.OTEL_SERVICE_NAME || "gh-aw";
diff --git a/setup/js/parse_antigravity_log.cjs b/setup/js/parse_antigravity_log.cjs
index f5bb0c16..cf38e08b 100644
--- a/setup/js/parse_antigravity_log.cjs
+++ b/setup/js/parse_antigravity_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, generateInformationSection } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, generateInformationSection, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Antigravity",
@@ -112,9 +112,11 @@ function parseAntigravityLog(logContent) {
});
}
+ const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "antigravity" });
+
return {
markdown,
- logEntries,
+ logEntries: canonicalLogEntries,
mcpFailures: [],
maxTurnsHit: false,
};
diff --git a/setup/js/parse_claude_log.cjs b/setup/js/parse_claude_log.cjs
index 1e20f934..b681975f 100644
--- a/setup/js/parse_claude_log.cjs
+++ b/setup/js/parse_claude_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Claude",
@@ -30,7 +30,8 @@ function parseClaudeLog(logContent) {
const mcpFailures = [];
// Generate conversation markdown using shared function
- const conversationResult = generateConversationMarkdown(logEntries, {
+ const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "claude" });
+ const conversationResult = generateConversationMarkdown(canonicalLogEntries, {
formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }),
formatInitCallback: initEntry => {
const result = formatInitializationSummary(initEntry, {
@@ -98,7 +99,7 @@ function parseClaudeLog(logContent) {
}
}
- return { markdown, mcpFailures, maxTurnsHit, logEntries };
+ return { markdown, mcpFailures, maxTurnsHit, logEntries: canonicalLogEntries };
}
// Export for testing
diff --git a/setup/js/parse_codex_log.cjs b/setup/js/parse_codex_log.cjs
index 36a88218..e61c1e56 100644
--- a/setup/js/parse_codex_log.cjs
+++ b/setup/js/parse_codex_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, truncateString, estimateTokens, formatToolCallAsDetails } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, truncateString, estimateTokens, formatToolCallAsDetails, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Codex",
@@ -643,9 +643,11 @@ function parseCodexLog(logContent) {
// Check for MCP failures
const mcpFailures = mcpInfo.servers.filter(server => server.status === "failed").map(server => server.name);
+ const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "codex" });
+
return {
markdown,
- logEntries,
+ logEntries: canonicalLogEntries,
mcpFailures,
maxTurnsHit: false, // Codex doesn't have max-turns concept in logs
};
diff --git a/setup/js/parse_copilot_log.cjs b/setup/js/parse_copilot_log.cjs
index dc1388e9..c94b994e 100644
--- a/setup/js/parse_copilot_log.cjs
+++ b/setup/js/parse_copilot_log.cjs
@@ -1,7 +1,18 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries, AWF_INFRA_LINE_RE } = require("./log_parser_shared.cjs");
+const {
+ createEngineLogParser,
+ generateConversationMarkdown,
+ generateInformationSection,
+ formatInitializationSummary,
+ formatToolUse,
+ parseLogEntries,
+ AWF_INFRA_LINE_RE,
+ isCopilotEventLogEntries,
+ convertLegacyLogEntriesToCopilotEvents,
+ convertCopilotEventsToLegacyLogEntries,
+} = require("./log_parser_shared.cjs");
const { ERR_PARSE } = require("./error_codes.cjs");
const main = createEngineLogParser({
@@ -52,10 +63,12 @@ function extractAwfTokenWarnings(logEntries) {
if (typeof value.message === "string") addMatches(value.message);
if (typeof value.content === "string") addMatches(value.content);
if (typeof value.system === "string") addMatches(value.system);
+ if (typeof value.data?.content === "string") addMatches(value.data.content);
if (Array.isArray(value.content)) visit(value.content);
if (Array.isArray(value.message?.content)) visit(value.message.content);
if (Array.isArray(value.system)) visit(value.system);
+ if (value.data && typeof value.data === "object") visit(value.data);
};
for (const entry of logEntries) visit(entry);
@@ -69,208 +82,7 @@ function extractAwfTokenWarnings(logEntries) {
* @returns {boolean}
*/
function isCopilotSdkEventsFormat(logEntries) {
- if (!Array.isArray(logEntries) || logEntries.length === 0) {
- return false;
- }
-
- const sdkEventTypes = new Set(["user.message", "assistant.message", "tool.execution_start", "tool.execution_complete", "reasoning", "assistant.reasoning"]);
- let sdkLikeCount = 0;
-
- for (const entry of logEntries) {
- if (!entry || typeof entry !== "object") continue;
- if (entry.type === "assistant" || entry.type === "user" || entry.type === "system" || entry.type === "result") {
- return false;
- }
- if (typeof entry.type === "string" && sdkEventTypes.has(entry.type) && typeof entry.data === "object" && entry.data !== null) {
- sdkLikeCount++;
- }
- }
-
- return sdkLikeCount > 0;
-}
-
-/**
- * Converts Copilot SDK events.jsonl entries into the normalized trace format
- * expected by generateConversationMarkdown and copilot-style summary renderers.
- * @param {Array} sdkEntries
- * @returns {Array}
- */
-function normalizeCopilotSdkEventsToTrace(sdkEntries) {
- /** @type {Array} */
- const normalizedEntries = [];
- const toolNames = new Set();
- const pendingByToolCallId = new Map();
- const pendingIdsByToolName = new Map();
- let toolCounter = 0;
- let turnCount = 0;
- let assistantMessageCount = 0;
- let firstTimestampMs = null;
- let lastTimestampMs = null;
-
- const addPendingId = (toolName, toolId) => {
- const existing = pendingIdsByToolName.get(toolName);
- if (existing) {
- existing.push(toolId);
- return;
- }
- pendingIdsByToolName.set(toolName, [toolId]);
- };
-
- const shiftPendingId = toolName => {
- const existing = pendingIdsByToolName.get(toolName);
- if (!existing || existing.length === 0) return null;
- const toolId = existing.shift();
- if (existing.length === 0) {
- pendingIdsByToolName.delete(toolName);
- }
- return toolId || null;
- };
-
- const removePendingId = (toolName, toolId) => {
- const existing = pendingIdsByToolName.get(toolName);
- if (!existing || existing.length === 0) return;
- const idx = existing.indexOf(toolId);
- if (idx === -1) return;
- existing.splice(idx, 1);
- if (existing.length === 0) {
- pendingIdsByToolName.delete(toolName);
- }
- };
-
- const normalizeToolName = (rawToolName, mcpServerName) => {
- const toolName = typeof rawToolName === "string" && rawToolName.trim() ? rawToolName.trim() : "unknown";
- if (toolName.startsWith("mcp__")) {
- return toolName;
- }
- const serverName = typeof mcpServerName === "string" ? mcpServerName.trim() : "";
- if (!serverName) {
- return toolName;
- }
- return `mcp__${serverName}__${toolName}`;
- };
-
- const maybeTrackTimestamp = timestamp => {
- if (typeof timestamp !== "string") return;
- const ms = new Date(timestamp).getTime();
- if (!Number.isFinite(ms)) return;
- if (firstTimestampMs === null || ms < firstTimestampMs) {
- firstTimestampMs = ms;
- }
- if (lastTimestampMs === null || ms > lastTimestampMs) {
- lastTimestampMs = ms;
- }
- };
-
- for (const entry of sdkEntries) {
- if (!entry || typeof entry !== "object") continue;
- maybeTrackTimestamp(entry.timestamp);
-
- switch (entry.type) {
- case "user.message":
- turnCount++;
- break;
-
- case "assistant.message": {
- assistantMessageCount++;
- const text = typeof entry.data?.content === "string" ? entry.data.content.trim() : "";
- if (!text) break;
- normalizedEntries.push({
- type: "assistant",
- message: {
- content: [{ type: "text", text }],
- },
- });
- break;
- }
-
- case "tool.execution_start": {
- const toolName = normalizeToolName(entry.data?.toolName, entry.data?.mcpServerName);
- toolNames.add(toolName);
- const toolCallId = typeof entry.data?.toolCallId === "string" && entry.data.toolCallId.trim() ? entry.data.toolCallId : null;
- const resolvedToolId = toolCallId || `sdk_tool_${++toolCounter}`;
- if (toolCallId) {
- pendingByToolCallId.set(toolCallId, resolvedToolId);
- }
- addPendingId(toolName, resolvedToolId);
- normalizedEntries.push({
- type: "assistant",
- message: {
- content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: {} }],
- },
- });
- break;
- }
-
- case "tool.execution_complete": {
- const toolName = normalizeToolName(entry.data?.toolName, entry.data?.mcpServerName);
- toolNames.add(toolName);
- const toolCallId = typeof entry.data?.toolCallId === "string" && entry.data.toolCallId.trim() ? entry.data.toolCallId : null;
- let resolvedToolId = null;
-
- if (toolCallId && pendingByToolCallId.has(toolCallId)) {
- resolvedToolId = pendingByToolCallId.get(toolCallId);
- pendingByToolCallId.delete(toolCallId);
- if (resolvedToolId) {
- removePendingId(toolName, resolvedToolId);
- }
- }
- if (!resolvedToolId) {
- resolvedToolId = shiftPendingId(toolName);
- }
- if (!resolvedToolId) {
- resolvedToolId = `sdk_tool_${++toolCounter}`;
- normalizedEntries.push({
- type: "assistant",
- message: {
- content: [{ type: "tool_use", id: resolvedToolId, name: toolName, input: {} }],
- },
- });
- }
-
- const success = typeof entry.data?.success === "boolean" ? entry.data.success : !entry.data?.error;
- normalizedEntries.push({
- type: "user",
- message: {
- content: [
- {
- type: "tool_result",
- tool_use_id: resolvedToolId,
- content: success ? "success" : "Tool execution failed",
- is_error: !success,
- },
- ],
- },
- });
- break;
- }
-
- default:
- break;
- }
- }
-
- if (normalizedEntries.length === 0) {
- return [];
- }
-
- normalizedEntries.unshift({
- type: "system",
- subtype: "init",
- model: "copilot-sdk",
- session_id: null,
- tools: Array.from(toolNames),
- });
-
- const resultEntry = {
- type: "result",
- num_turns: turnCount > 0 ? turnCount : assistantMessageCount,
- };
- if (firstTimestampMs !== null && lastTimestampMs !== null && lastTimestampMs >= firstTimestampMs) {
- resultEntry.duration_ms = lastTimestampMs - firstTimestampMs;
- }
- normalizedEntries.push(resultEntry);
-
- return normalizedEntries;
+ return isCopilotEventLogEntries(logEntries);
}
/**
@@ -310,15 +122,26 @@ function parseCopilotLog(logContent) {
return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] };
}
- if (isCopilotSdkEventsFormat(logEntries)) {
- const normalized = normalizeCopilotSdkEventsToTrace(logEntries);
- if (normalized.length > 0) {
- logEntries = normalized;
- }
+ const isEventFormat = isCopilotSdkEventsFormat(logEntries);
+ let canonicalLogEntries = isEventFormat ? logEntries : convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "copilot" });
+ const legacyRenderEntries = isEventFormat ? convertCopilotEventsToLegacyLogEntries(canonicalLogEntries) : logEntries;
+ if (isEventFormat && !canonicalLogEntries.some(entry => entry?.type === "session.result")) {
+ const legacyResult = legacyRenderEntries.find(entry => entry?.type === "result");
+ canonicalLogEntries.push({
+ type: "session.result",
+ data: {
+ numTurns: legacyResult?.num_turns,
+ durationMs: legacyResult?.duration_ms,
+ totalCostUsd: legacyResult?.total_cost_usd,
+ usage: legacyResult?.usage,
+ errors: legacyResult?.errors,
+ permissionDenials: legacyResult?.permission_denials,
+ },
+ });
}
// Generate conversation markdown using shared function
- const conversationResult = generateConversationMarkdown(logEntries, {
+ const conversationResult = generateConversationMarkdown(canonicalLogEntries, {
formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: true }),
formatInitCallback: initEntry =>
formatInitializationSummary(initEntry, {
@@ -364,7 +187,7 @@ function parseCopilotLog(logContent) {
});
let markdown = conversationResult.markdown;
- const awfTokenWarnings = extractAwfTokenWarnings(logEntries);
+ const awfTokenWarnings = extractAwfTokenWarnings(canonicalLogEntries);
if (awfTokenWarnings.length > 0) {
markdown += "## ⚠️ Firewall Steering\n\n";
@@ -375,14 +198,13 @@ function parseCopilotLog(logContent) {
}
// Add Information section
- const lastEntry = logEntries[logEntries.length - 1];
- const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init");
+ const lastEntry = legacyRenderEntries[legacyRenderEntries.length - 1];
markdown += generateInformationSection(lastEntry, {
additionalInfoCallback: () => "",
});
- return { markdown, logEntries };
+ return { markdown, logEntries: canonicalLogEntries };
}
/**
diff --git a/setup/js/parse_gemini_log.cjs b/setup/js/parse_gemini_log.cjs
index 30b38a06..9c3615ee 100644
--- a/setup/js/parse_gemini_log.cjs
+++ b/setup/js/parse_gemini_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Gemini",
@@ -61,7 +61,8 @@ function parseGeminiLog(logContent) {
const resultEntry = rawEntries.find(e => e.type === "result");
// Generate conversation markdown using shared function
- const conversationResult = generateConversationMarkdown(logEntries, {
+ const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "gemini" });
+ const conversationResult = generateConversationMarkdown(canonicalLogEntries, {
formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }),
formatInitCallback: initEntry => formatInitializationSummary(initEntry, { includeSlashCommands: false }),
});
@@ -87,7 +88,7 @@ function parseGeminiLog(logContent) {
return {
markdown,
- logEntries,
+ logEntries: canonicalLogEntries,
mcpFailures: [],
maxTurnsHit: false,
};
diff --git a/setup/js/parse_pi_log.cjs b/setup/js/parse_pi_log.cjs
index 31f3cf28..52d539a9 100644
--- a/setup/js/parse_pi_log.cjs
+++ b/setup/js/parse_pi_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Pi",
@@ -57,7 +57,8 @@ function parsePiLog(logContent) {
const resultEntry = rawEntries.find(e => e.type === "result");
- const conversationResult = generateConversationMarkdown(logEntries, {
+ const canonicalLogEntries = convertLegacyLogEntriesToCopilotEvents(logEntries, { sourceEngine: "pi" });
+ const conversationResult = generateConversationMarkdown(canonicalLogEntries, {
formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: false }),
formatInitCallback: initEntry => formatInitializationSummary(initEntry, { includeSlashCommands: false }),
});
@@ -81,7 +82,7 @@ function parsePiLog(logContent) {
return {
markdown,
- logEntries,
+ logEntries: canonicalLogEntries,
mcpFailures: [],
maxTurnsHit: false,
};
diff --git a/setup/js/safe_output_type_validator.cjs b/setup/js/safe_output_type_validator.cjs
index d9de7359..7f47cb7d 100644
--- a/setup/js/safe_output_type_validator.cjs
+++ b/setup/js/safe_output_type_validator.cjs
@@ -409,6 +409,15 @@ function validateField(value, fieldName, validation, itemType, lineNum, options)
}
if (validation.type === "array") {
+ // Backward compatibility: create_issue agents sometimes provide comma-separated labels as a string.
+ // Normalize this into a string array before strict array validation.
+ if (itemType === "create_issue" && fieldName === "labels" && typeof value === "string") {
+ value = value
+ .split(",")
+ .map(item => item.trim())
+ .filter(Boolean);
+ }
+
if (!Array.isArray(value)) {
// For required fields, use "requires a" format for both missing and wrong type
if (validation.required) {
diff --git a/setup/js/safe_outputs_tools.json b/setup/js/safe_outputs_tools.json
index 8746d177..60d9d499 100644
--- a/setup/js/safe_outputs_tools.json
+++ b/setup/js/safe_outputs_tools.json
@@ -1,7 +1,7 @@
[
{
"name": "create_issue",
- "description": "WRITE-ONCE: do NOT call this tool with empty or placeholder arguments to probe or discover its schema \u2014 required fields (title, body) are listed in this schema; if you are not ready to open the real issue, call `noop` instead. Creates a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead.",
+ "description": "WRITE-ONCE: do NOT call this tool with empty or placeholder arguments to probe or discover its schema \u2014 required fields (title, body) are listed in this schema; if you are not ready to open the real issue, call `noop` instead. Creates a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. Compatibility: labels may be passed as either an array of strings or a comma-separated string; string input is split, trimmed, and normalized to an array.",
"inputSchema": {
"type": "object",
"required": ["title", "body"],
@@ -16,11 +16,11 @@
"description": "Detailed issue description in Markdown. Must be the final intended body \u2014 not a placeholder or test value. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate."
},
"labels": {
- "type": "array",
+ "type": ["array", "string"],
"items": {
"type": "string"
},
- "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository."
+ "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository. Accepts either an array of strings or a comma-separated string; string input is split on commas, trimmed, and normalized to an array."
},
"fields": {
"type": "array",
diff --git a/setup/js/send_otlp_span.cjs b/setup/js/send_otlp_span.cjs
index 83793c36..afd85639 100644
--- a/setup/js/send_otlp_span.cjs
+++ b/setup/js/send_otlp_span.cjs
@@ -1282,7 +1282,8 @@ async function sendJobSetupSpan(options = {}) {
const runnerArch = process.env.RUNNER_ARCH || "";
const runnerName = process.env.RUNNER_NAME || "";
const runnerEnvironment = process.env.RUNNER_ENVIRONMENT || "";
- const scopeVersion = process.env.GH_AW_INFO_VERSION || process.env.GH_AW_INFO_CLI_VERSION || process.env.GITHUB_SHA || "unknown";
+ const scopeVersion =
+ (typeof awInfo.cli_version === "string" ? awInfo.cli_version : "") || process.env.GH_AW_INFO_CLI_VERSION || awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || process.env.GITHUB_SHA || "unknown";
const attributes = [
buildAttr("gh-aw.job.name", jobName),
@@ -1937,7 +1938,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
const awInfo = readJSONIfExists("/tmp/gh-aw/aw_info.json") || {};
const serviceName = process.env.OTEL_SERVICE_NAME || "gh-aw";
- const version = awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || awInfo.cli_version || process.env.GH_AW_INFO_CLI_VERSION || process.env.GITHUB_SHA || "unknown";
+ const version = (typeof awInfo.cli_version === "string" ? awInfo.cli_version : "") || process.env.GH_AW_INFO_CLI_VERSION || awInfo.agent_version || awInfo.version || process.env.GH_AW_INFO_VERSION || process.env.GITHUB_SHA || "unknown";
// Prefer GITHUB_AW_OTEL_TRACE_ID (written to GITHUB_ENV by this job's setup step) so
// all spans in the same job share one trace. Fall back to aw_context.otel_trace_id
diff --git a/setup/js/set_issue_field.cjs b/setup/js/set_issue_field.cjs
index 73928fbf..292c5534 100644
--- a/setup/js/set_issue_field.cjs
+++ b/setup/js/set_issue_field.cjs
@@ -41,74 +41,50 @@ async function getIssueNodeId(githubClient, owner, repo, issueNumber) {
* @returns {Promise}>>}
*/
async function fetchIssueFields(githubClient, owner, repo) {
- try {
- const result = await githubClient.graphql(
- `query($owner: String!, $repo: String!) {
- repository(owner: $owner, name: $repo) {
- issueFields(first: 100) {
- nodes {
- __typename
- id
- name
- ... on IssueFieldSingleSelect {
- options {
- id
- name
- }
- }
- }
- }
- owner {
+ const result = await githubClient.graphql(
+ `query($owner: String!, $repo: String!) {
+ repository(owner: $owner, name: $repo) {
+ issueFields(first: 100) {
+ nodes {
__typename
- ... on Organization {
- issueFields(first: 100) {
- nodes {
- __typename
- id
- name
- ... on IssueFieldSingleSelect {
- options {
- id
- name
- }
- }
- }
- }
- }
- ... on User {
- issueFields(first: 100) {
- nodes {
- __typename
- id
- name
- ... on IssueFieldSingleSelect {
- options {
- id
- name
- }
- }
- }
+ ... on IssueField { id name }
+ ... on IssueFieldText { id name }
+ ... on IssueFieldNumber { id name }
+ ... on IssueFieldDate { id name }
+ ... on IssueFieldSingleSelect { id name options { id name } }
+ ... on IssueFieldMultiSelect { id name options { id name } }
+ }
+ }
+ owner {
+ __typename
+ ... on Organization {
+ issueFields(first: 100) {
+ nodes {
+ __typename
+ ... on IssueField { id name }
+ ... on IssueFieldText { id name }
+ ... on IssueFieldNumber { id name }
+ ... on IssueFieldDate { id name }
+ ... on IssueFieldSingleSelect { id name options { id name } }
+ ... on IssueFieldMultiSelect { id name options { id name } }
}
}
}
}
- }`,
- { owner, repo }
- );
+ }
+ }`,
+ { owner, repo }
+ );
- const repoFields = result?.repository?.issueFields?.nodes ?? [];
- if (repoFields.length > 0) {
- return repoFields;
- }
+ const isValidNode = node => typeof node?.id === "string" && typeof node?.name === "string";
- const ownerFields = result?.repository?.owner?.issueFields?.nodes ?? [];
- return ownerFields;
- } catch (error) {
- if (typeof core !== "undefined") {
- core.debug(`Could not fetch issue fields (may not be enabled): ${error instanceof Error ? error.message : String(error)}`);
- }
- return [];
+ const repoFields = (result?.repository?.issueFields?.nodes ?? []).filter(isValidNode);
+ if (repoFields.length > 0) {
+ return repoFields;
}
+
+ const ownerFields = (result?.repository?.owner?.issueFields?.nodes ?? []).filter(isValidNode);
+ return ownerFields;
}
/**
diff --git a/setup/js/start_mcp_gateway.cjs b/setup/js/start_mcp_gateway.cjs
index 6bdb7d97..43c487c9 100644
--- a/setup/js/start_mcp_gateway.cjs
+++ b/setup/js/start_mcp_gateway.cjs
@@ -208,6 +208,58 @@ function assertNotSymlink(p) {
}
}
+/**
+ * Resolves the Copilot CLI config directory and the MCP config file path from
+ * the runtime $HOME. The Copilot CLI uses ~/.copilot, which is
+ * /home/runner/.copilot on standard GitHub-hosted runners (HOME=/home/runner)
+ * but may differ on self-hosted or containerized runners. HOME is a standard
+ * POSIX environment variable inherited from the runner's parent process and
+ * passed through to shell steps; other generators (copilot_mcp.go,
+ * copilot_engine_execution.go) rely on it the same way.
+ *
+ * Exported for testability; throws Error rather than exiting so tests can
+ * exercise the missing-HOME branch.
+ *
+ * @returns {{ dir: string, file: string }}
+ */
+function resolveCopilotConfigPaths() {
+ const home = process.env.HOME;
+ if (!home) {
+ throw new Error("HOME environment variable is not set; cannot locate Copilot CLI config directory");
+ }
+ const dir = path.join(home, ".copilot");
+ return { dir, file: path.join(dir, "mcp-config.json") };
+}
+
+/**
+ * Determines which engine-specific MCP config converter to use.
+ * Copilot auto-detection consults HOME only when HOME is available so
+ * explicit non-Copilot engines do not fail just because HOME is unset.
+ *
+ * @param {string} configDir
+ * @param {NodeJS.ProcessEnv} [env]
+ * @param {(p: string) => boolean} [existsSync]
+ * @returns {string}
+ */
+function detectEngineType(configDir, env = process.env, existsSync = fs.existsSync) {
+ if (env.GH_AW_ENGINE) {
+ return env.GH_AW_ENGINE;
+ }
+ if (env.GITHUB_COPILOT_CLI_MODE) {
+ return "copilot";
+ }
+ if (env.HOME && existsSync(path.join(env.HOME, ".copilot"))) {
+ return "copilot";
+ }
+ if (existsSync(path.join(configDir, "config.toml"))) {
+ return "codex";
+ }
+ if (existsSync(path.join(configDir, "mcp-servers.json"))) {
+ return "claude";
+ }
+ return "unknown";
+}
+
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
@@ -671,18 +723,7 @@ async function main() {
}
// Determine engine type
- let engineType = process.env.GH_AW_ENGINE || "";
- if (!engineType) {
- if (fs.existsSync("/home/runner/.copilot") || process.env.GITHUB_COPILOT_CLI_MODE) {
- engineType = "copilot";
- } else if (fs.existsSync(path.join(configDir, "config.toml"))) {
- engineType = "codex";
- } else if (fs.existsSync(path.join(configDir, "mcp-servers.json"))) {
- engineType = "claude";
- } else {
- engineType = "unknown";
- }
- }
+ const engineType = detectEngineType(configDir);
core.info(`Detected engine type: ${engineType}`);
@@ -699,10 +740,17 @@ async function main() {
const converterPath = path.join(runnerTemp || "", "gh-aw/actions", converterFile);
execSync(`node "${converterPath}"`, { stdio: "inherit", env: process.env });
} else {
+ let copilotConfigDir, copilotConfigFile;
+ try {
+ ({ dir: copilotConfigDir, file: copilotConfigFile } = resolveCopilotConfigPaths());
+ } catch (err) {
+ core.error(`ERROR: ${err.message}`);
+ process.exit(1);
+ }
core.info(`No agent-specific converter found for engine: ${engineType}`);
core.info("Using gateway output directly");
// Default fallback – copy to most common location, filtering CLI-mounted servers
- fs.mkdirSync("/home/runner/.copilot", { recursive: true });
+ fs.mkdirSync(copilotConfigDir, { recursive: true });
const cliServersRaw = process.env.GH_AW_MCP_CLI_SERVERS;
if (cliServersRaw) {
try {
@@ -716,16 +764,16 @@ async function main() {
}
}
}
- fs.writeFileSync("/home/runner/.copilot/mcp-config.json", JSON.stringify(filtered, null, 2), { mode: 0o600 });
+ fs.writeFileSync(copilotConfigFile, JSON.stringify(filtered, null, 2), { mode: 0o600 });
} catch {
core.error("ERROR: Failed to filter CLI-mounted servers from agent MCP config");
core.info("Falling back to unfiltered config");
- fs.copyFileSync(outputPath, "/home/runner/.copilot/mcp-config.json");
+ fs.copyFileSync(outputPath, copilotConfigFile);
}
} else {
- fs.copyFileSync(outputPath, "/home/runner/.copilot/mcp-config.json");
+ fs.copyFileSync(outputPath, copilotConfigFile);
}
- core.info(fs.readFileSync("/home/runner/.copilot/mcp-config.json", "utf8"));
+ core.info(fs.readFileSync(copilotConfigFile, "utf8"));
}
printTiming(configConvertStart, "Configuration conversion");
core.info("");
@@ -842,7 +890,9 @@ if (require.main === module) {
module.exports = {
applyOTLPIgnoreIfMissing,
+ detectEngineType,
getOTLPIfMissingMode,
hasNonEmptyOTLPHeaders,
isOTLPIfMissingIgnore,
+ resolveCopilotConfigPaths,
};
diff --git a/setup/js/validate_context_variables.cjs b/setup/js/validate_context_variables.cjs
index 2f3b78c8..45481ac4 100644
--- a/setup/js/validate_context_variables.cjs
+++ b/setup/js/validate_context_variables.cjs
@@ -45,7 +45,6 @@
* not a numeric database ID. Push events always produce a hex SHA here.
*/
-const { getErrorMessage } = require("./error_helpers.cjs");
const { ERR_VALIDATION } = require("./error_codes.cjs");
/**
@@ -146,58 +145,44 @@ function validateNumericValue(value, varName) {
* @param {object} ctx - GitHub Actions context object
*/
async function validateContextVariables(coreArg, ctx) {
- try {
- coreArg.info("Starting context variable validation...");
-
- const failures = [];
- let checkedCount = 0;
-
- // Validate each numeric context variable by reading directly from context
- for (const { path, name } of NUMERIC_CONTEXT_PATHS) {
- const value = getNestedValue(ctx, path);
-
- // Only validate if the value exists
- if (value !== undefined) {
- checkedCount++;
- const result = validateNumericValue(value, name);
-
- if (result.valid) {
- coreArg.info(`✓ ${result.message}`);
- } else {
- coreArg.error(`✗ ${result.message}`);
- failures.push({
- name,
- value,
- message: result.message,
- });
- }
+ coreArg.info("Starting context variable validation...");
+
+ const failures = [];
+ let checkedCount = 0;
+
+ for (const { path, name } of NUMERIC_CONTEXT_PATHS) {
+ const value = getNestedValue(ctx, path);
+ if (value !== undefined) {
+ checkedCount++;
+ const result = validateNumericValue(value, name);
+ if (result.valid) {
+ coreArg.info(`✓ ${result.message}`);
+ } else {
+ coreArg.error(`✗ ${result.message}`);
+ failures.push({ name, value, message: result.message });
}
}
+ }
- coreArg.info(`Validated ${checkedCount} context variables`);
-
- // If there are any failures, fail the workflow
- if (failures.length > 0) {
- const errorMessage =
- `Context variable validation failed!\n\n` +
- `Found ${failures.length} malicious or invalid numeric field(s):\n\n` +
- failures.map(f => ` - ${f.name}: "${f.value}"\n ${f.message}`).join("\n\n") +
- "\n\n" +
- "Numeric context variables (like github.event.issue.number) must be either empty or valid integers.\n" +
- "This validation prevents injection attacks where special text or code is hidden in numeric fields.\n\n" +
- "If you believe this is a false positive, please report it at:\n" +
- "https://github.com/github/gh-aw/issues";
-
- coreArg.setFailed(errorMessage);
- throw new Error(errorMessage);
- }
-
- coreArg.info("✅ All context variables validated successfully");
- } catch (error) {
- const errorMessage = getErrorMessage(error);
- coreArg.setFailed(`${ERR_VALIDATION}: Context variable validation failed: ${errorMessage}`);
- throw error;
+ coreArg.info(`Validated ${checkedCount} context variables`);
+
+ if (failures.length > 0) {
+ const failureDetails = failures.map(f => ` - ${f.name}: "${f.value}"\n ${f.message}`).join("\n\n");
+ const errorMessage =
+ `${ERR_VALIDATION}: Context variable validation failed!\n\n` +
+ `Found ${failures.length} malicious or invalid numeric field(s):\n\n` +
+ failureDetails +
+ "\n\n" +
+ "Numeric context variables (like github.event.issue.number) must be either empty or valid integers.\n" +
+ "This validation prevents injection attacks where special text or code is hidden in numeric fields.\n\n" +
+ "If you believe this is a false positive, please report it at:\n" +
+ "https://github.com/github/gh-aw/issues";
+
+ coreArg.setFailed(errorMessage);
+ throw new Error(errorMessage);
}
+
+ coreArg.info("✅ All context variables validated successfully");
}
/**
diff --git a/setup/post.js b/setup/post.js
index 6fbd5482..2834ca97 100644
--- a/setup/post.js
+++ b/setup/post.js
@@ -13,6 +13,75 @@ const path = require("path");
const { spawnSync } = require("child_process");
const fs = require("fs");
+function isDebugModeEnabled() {
+ const toBool = (value) => {
+ const normalized = String(value || "").toLowerCase();
+ return normalized === "1" || normalized === "true";
+ };
+ return toBool(process.env.RUNNER_DEBUG) || toBool(process.env.ACTIONS_STEP_DEBUG);
+}
+
+function listTmpGhAwFiles(tmpDir, maxDepth, maxFiles) {
+ if (!fs.existsSync(tmpDir)) {
+ console.log(`[debug] ${tmpDir} does not exist; skipping file listing`);
+ return;
+ }
+
+ const files = [];
+ let readErrors = 0;
+
+ const walk = (currentDir, depth) => {
+ if (depth >= maxDepth || files.length >= maxFiles) {
+ return;
+ }
+
+ let entries;
+ try {
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
+ } catch (err) {
+ readErrors += 1;
+ console.log(`[debug] failed to read ${currentDir}: ${err.message}`);
+ return;
+ }
+
+ entries.sort((a, b) => a.name.localeCompare(b.name));
+
+ for (const entry of entries) {
+ if (files.length >= maxFiles) {
+ return;
+ }
+
+ const fullPath = path.join(currentDir, entry.name);
+ if (entry.isDirectory()) {
+ walk(fullPath, depth + 1);
+ continue;
+ }
+
+ files.push(path.relative(tmpDir, fullPath) || ".");
+ }
+ };
+
+ walk(tmpDir, 0);
+
+ const truncated = files.length >= maxFiles;
+ console.log(
+ `[debug] listing files under ${tmpDir} (max depth ${maxDepth}, max files ${maxFiles})`,
+ );
+ if (files.length === 0) {
+ console.log("[debug] no files found");
+ } else {
+ for (const file of files) {
+ console.log(`[debug] - ${file}`);
+ }
+ }
+ if (truncated) {
+ console.log(`[debug] output truncated at ${maxFiles} files`);
+ }
+ if (readErrors > 0) {
+ console.log(`[debug] encountered ${readErrors} directory read error(s)`);
+ }
+}
+
// Wrap everything in an async IIFE so that the OTLP span is fully sent before
// the cleanup deletes /tmp/gh-aw/ (which contains aw_info.json and otel.jsonl).
(async () => {
@@ -28,6 +97,12 @@ const fs = require("fs");
}
const tmpDir = "/tmp/gh-aw";
+ const maxDebugDepth = 4;
+ const maxDebugFiles = 200;
+
+ if (isDebugModeEnabled()) {
+ listTmpGhAwFiles(tmpDir, maxDebugDepth, maxDebugFiles);
+ }
console.log(`Cleaning up ${tmpDir}...`);
diff --git a/setup/sh/convert_gateway_config_copilot.sh b/setup/sh/convert_gateway_config_copilot.sh
index add43b66..190043d7 100755
--- a/setup/sh/convert_gateway_config_copilot.sh
+++ b/setup/sh/convert_gateway_config_copilot.sh
@@ -38,6 +38,15 @@ if [ -z "$MCP_GATEWAY_PORT" ]; then
exit 1
fi
+if [ -z "${HOME:-}" ]; then
+ echo "ERROR: HOME environment variable is required"
+ exit 1
+fi
+
+COPILOT_CONFIG_DIR="$HOME/.copilot"
+COPILOT_CONFIG_PATH="$COPILOT_CONFIG_DIR/mcp-config.json"
+mkdir -p "$COPILOT_CONFIG_DIR"
+
echo "Converting gateway configuration to Copilot format..."
echo "Input: $MCP_GATEWAY_OUTPUT"
echo "Target domain: $MCP_GATEWAY_DOMAIN:$MCP_GATEWAY_PORT"
@@ -88,15 +97,15 @@ jq --arg urlPrefix "$URL_PREFIX" '
.url |= (. | sub("^http://[^/]+/mcp/"; $urlPrefix + "/mcp/"))
)
)
-' "$MCP_GATEWAY_OUTPUT" > /home/runner/.copilot/mcp-config.json
+' "$MCP_GATEWAY_OUTPUT" > "$COPILOT_CONFIG_PATH"
# Restrict permissions so only the runner process owner can read this file.
# mcp-config.json contains the bearer token for the MCP gateway; an attacker
# who reads it could bypass the --allowed-tools constraint by issuing raw
# JSON-RPC calls directly to the gateway.
-chmod 600 /home/runner/.copilot/mcp-config.json
+chmod 600 "$COPILOT_CONFIG_PATH"
-echo "Copilot configuration written to /home/runner/.copilot/mcp-config.json"
+echo "Copilot configuration written to $COPILOT_CONFIG_PATH"
echo ""
echo "Converted configuration:"
-cat /home/runner/.copilot/mcp-config.json
+cat "$COPILOT_CONFIG_PATH"
diff --git a/setup/sh/start_mcp_gateway.sh b/setup/sh/start_mcp_gateway.sh
index e3e3aeb2..410a6cf2 100755
--- a/setup/sh/start_mcp_gateway.sh
+++ b/setup/sh/start_mcp_gateway.sh
@@ -376,11 +376,20 @@ if [ -z "$MCP_GATEWAY_API_KEY" ]; then
exit 1
fi
+require_home_for_copilot_config() {
+ if [ -z "${HOME:-}" ]; then
+ echo "ERROR: HOME environment variable must be set before using Copilot MCP config paths"
+ exit 1
+ fi
+}
+
# Determine which agent-specific converter to use based on engine type
# Check for engine-specific indicators and call appropriate converter
if [ -n "$GH_AW_ENGINE" ]; then
ENGINE_TYPE="$GH_AW_ENGINE"
-elif [ -f "/home/runner/.copilot" ] || [ -n "$GITHUB_COPILOT_CLI_MODE" ]; then
+elif [ -n "$GITHUB_COPILOT_CLI_MODE" ]; then
+ ENGINE_TYPE="copilot"
+elif [ -n "${HOME:-}" ] && [ -d "$HOME/.copilot" ]; then
ENGINE_TYPE="copilot"
elif [ -f "/tmp/gh-aw/mcp-config/config.toml" ]; then
ENGINE_TYPE="codex"
@@ -417,19 +426,20 @@ case "$ENGINE_TYPE" in
echo "No agent-specific converter found for engine: $ENGINE_TYPE"
echo "Using gateway output directly"
# Default fallback - copy to most common location, filtering out CLI-mounted servers
- mkdir -p /home/runner/.copilot
+ require_home_for_copilot_config
+ mkdir -p "$HOME/.copilot"
if [ -n "$GH_AW_MCP_CLI_SERVERS" ]; then
if ! jq --argjson cliServers "$GH_AW_MCP_CLI_SERVERS" \
'.mcpServers |= with_entries(select(.key | IN($cliServers[]) | not))' \
- /tmp/gh-aw/mcp-config/gateway-output.json > /home/runner/.copilot/mcp-config.json; then
+ /tmp/gh-aw/mcp-config/gateway-output.json > "$HOME/.copilot/mcp-config.json"; then
echo "ERROR: Failed to filter CLI-mounted servers from agent MCP config"
echo "Falling back to unfiltered config"
- cp /tmp/gh-aw/mcp-config/gateway-output.json /home/runner/.copilot/mcp-config.json
+ cp /tmp/gh-aw/mcp-config/gateway-output.json "$HOME/.copilot/mcp-config.json"
fi
else
- cp /tmp/gh-aw/mcp-config/gateway-output.json /home/runner/.copilot/mcp-config.json
+ cp /tmp/gh-aw/mcp-config/gateway-output.json "$HOME/.copilot/mcp-config.json"
fi
- cat /home/runner/.copilot/mcp-config.json
+ cat "$HOME/.copilot/mcp-config.json"
;;
esac
print_timing $CONFIG_CONVERT_START "Configuration conversion"