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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions setup/js/ai_credits_context.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 };
}
Expand Down
1 change: 1 addition & 0 deletions setup/js/apply_samples.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
// @ts-check
/// <reference types="@actions/github-script" />
// @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
//
Expand Down
6 changes: 6 additions & 0 deletions setup/js/artifact_client.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// @ts-check
/// <reference types="@actions/github-script" />

/**
* @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");
Expand Down
19 changes: 12 additions & 7 deletions setup/js/check_daily_aic_workflow_guardrail.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -412,15 +418,18 @@ 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) {
truncatedByRateLimit = true;
break;
}
}
if (candidateRuns.length >= maxInspectableRuns || runs.length < 100) {
if (reachedCutoff || candidateRuns.length >= maxInspectableRuns || runs.length < 100) {
break;
}
page += 1;
Expand All @@ -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) {
Expand Down
41 changes: 36 additions & 5 deletions setup/js/convert_gateway_config_copilot.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>} entry
Expand All @@ -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...");
Expand All @@ -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);
Expand All @@ -70,4 +101,4 @@ if (require.main === module) {
main();
}

module.exports = { rewriteUrl, transformCopilotEntry, main };
module.exports = { rewriteUrl, transformCopilotEntry, resolveCopilotConfigOutputPath, main };
10 changes: 5 additions & 5 deletions setup/js/emit_outcome_spans.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 5 additions & 2 deletions setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}`);
Expand Down
4 changes: 2 additions & 2 deletions setup/js/log_parser_bootstrap.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 20 additions & 7 deletions setup/js/log_parser_format.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* @property {(text: string) => number} estimateTokens
* @property {(ms: number) => string} formatDuration
* @property {(text: string) => string} unfenceMarkdown
* @property {(entries: Array<any>) => boolean} isCopilotEventLogEntries
* @property {(entries: Array<any>) => Array<any>} convertCopilotEventsToLegacyLogEntries
* @property {number} MAX_AGENT_TEXT_LENGTH
* @property {string} SIZE_LIMIT_WARNING
*/
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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")) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -569,7 +582,7 @@ function createLogParserFormatters(deps) {
lines.push("");
}

appendStatistics(lines, logEntries, toolUsePairs);
appendStatistics(lines, renderEntries, toolUsePairs);

return lines;
}
Expand Down
Loading
Loading