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
2 changes: 1 addition & 1 deletion .github/aw/compat.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"min-gh-aw": "0.72.0",
"max-gh-aw": "*",
"min-agent": "1.0.21",
"max-agent": "1.0.77",
"max-agent": "1.0.78",
"open": true
},
{
Expand Down
2 changes: 2 additions & 0 deletions setup/js/action_setup_otlp.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require("./shim.cjs");
const { appendFileSync } = require("fs");
const { nowMs } = require("./performance_now.cjs");
const { getActionInput } = require("./action_input_utils.cjs");
const { maskSecret } = require("./actions_secret_masking.cjs");

/**
* Append a key=value line to a GitHub Actions file (GITHUB_OUTPUT or GITHUB_ENV)
Expand Down Expand Up @@ -134,6 +135,7 @@ async function run() {

const inputOTLPOIDCToken = getActionInput("OTLP_OIDC_TOKEN");
if (inputOTLPOIDCToken) {
maskSecret(inputOTLPOIDCToken);
const existingHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS || "";
const mergedHeaders = mergeAuthorizationHeader(existingHeaders, inputOTLPOIDCToken);

Expand Down
40 changes: 40 additions & 0 deletions setup/js/actions_secret_masking.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @ts-check

/**
* Escape a value for a GitHub Actions workflow command payload.
*
* @param {string} value
* @returns {string}
*/
function escapeWorkflowCommandValue(value) {
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
}

/**
* Mask a secret in the surrounding GitHub Actions step when masking is available.
*
* Plain Node entry points such as setup.sh-loaded scripts do not have the real
* @actions/core object, but GitHub Actions still processes add-mask workflow
* commands emitted by the process.
*
* @param {unknown} value
*/
function maskSecret(value) {
if (value === undefined || value === null) return;
const secret = String(value);
if (!secret) return;

const setSecret = global.core?.setSecret;
if (typeof setSecret === "function" && !setSecret.__ghAwUnavailable) {
setSecret.call(global.core, secret);
return;
}

// shim.cjs marks its throwing placeholder so Actions-side plain Node callers
// can still mask via workflow commands without enabling masking in MCP shims.
if (process.env.GITHUB_ACTIONS === "true") {
process.stdout.write(`::add-mask::${escapeWorkflowCommandValue(secret)}\n`);
}
}

module.exports = { escapeWorkflowCommandValue, maskSecret };
93 changes: 87 additions & 6 deletions setup/js/add_labels.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const { MAX_LABELS } = require("./constants.cjs");
const { createCountGatedHandler } = require("./handler_scaffold.cjs");
const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs");
const { resolveInvocationContext } = require("./invocation_context_helpers.cjs");
const { normalizeIssueIntentLabelInputs } = require("./issue_intents.cjs");
const { normalizeIssueIntentLabelInputs, buildIssueIntentLabelUpdates } = require("./issue_intents.cjs");
const { fetchAllRepoLabels } = require("./github_api_helpers.cjs");

/**
* @param {{ rationale?: string, confidence?: string, suggest?: boolean } | null | undefined} spec
Expand Down Expand Up @@ -223,11 +224,18 @@ const main = createCountGatedHandler({
};
}

const labelsRequestPayload = uniqueLabels.map(name => {
const labelSpec = requestedLabelSpecByLowerName.get(name.toLowerCase()) ?? { name };
const hasIntentMetadata = hasLabelIntentMetadata(labelSpec);
return issueIntentEnabled && hasIntentMetadata ? labelSpec : labelSpec.name;
});
// Build the resolved label specs (name + optional intent metadata) for the validated
// unique labels, preserving the order returned by validation.
const uniqueLabelSpecs = uniqueLabels.map(name => requestedLabelSpecByLowerName.get(name.toLowerCase()) ?? { name });
const intentLabelSpecs = uniqueLabelSpecs.filter(spec => hasLabelIntentMetadata(spec));
const useIssueIntentPath = issueIntentEnabled && intentLabelSpecs.length > 0;

// The REST issues.addLabels endpoint only accepts label name strings; it does not
// support issue-intent metadata (rationale/confidence/suggest). Passing objects with
// those extra keys causes GitHub to return success while silently applying no labels.
// When intent metadata is present, route through the GraphQL updateIssue/LabelUpdateInput
// mutation instead (see update_issue.cjs), which does support intent metadata.
const labelsRequestPayload = uniqueLabels;

core.info(`Adding ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo}: ${JSON.stringify(labelsRequestPayload)}`);

Expand All @@ -248,6 +256,79 @@ const main = createCountGatedHandler({

try {
const beforeState = await fetchIssueState(githubClient, repoParts, itemNumber);

if (useIssueIntentPath) {
// Intent metadata is only supported via the GraphQL updateIssue mutation. That
// mutation replaces the issue's label set, so merge the newly requested labels with
// the issue's existing labels to preserve add-only semantics. Existing labels are
// sent without intent metadata; newly requested labels carry their metadata.
const { data: issueData } = await withRetry(
() =>
githubClient.rest.issues.get({
owner: repoParts.owner,
repo: repoParts.repo,
issue_number: itemNumber,
}),
RATE_LIMIT_RETRY_CONFIG,
`get ${contextType} #${itemNumber} in ${itemRepo}`
);

const issueNodeId = issueData?.node_id;
if (!issueNodeId) {
throw new Error(`Failed to resolve GraphQL node ID for ${contextType} #${itemNumber}`);
}

const repoLabels = await fetchAllRepoLabels(githubClient, repoParts.owner, repoParts.repo);
const labelIdByName = new Map(repoLabels.map(label => [label.name.toLowerCase(), label.id]));

// Merge existing labels (metadata-free) with the requested specs, de-duplicating by
// lowercased name and favouring the requested specs so their intent metadata wins.
const requestedNamesLower = new Set(uniqueLabelSpecs.map(spec => spec.name.toLowerCase()));
const existingLabelNames = normalizeLabelNames(issueData.labels || []);
const mergedSpecs = [...uniqueLabelSpecs, ...existingLabelNames.filter(name => !requestedNamesLower.has(name.toLowerCase())).map(name => ({ name }))];

const labelIntentUpdates = buildIssueIntentLabelUpdates(mergedSpecs, labelIdByName);
Comment on lines +284 to +290

core.info(`Adding ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo} via GraphQL intent mutation`);
const result = await withRetry(
() =>
githubClient.graphql(
`mutation($issueId: ID!, $labels: [LabelUpdateInput!]!) {
updateIssue(input: { id: $issueId, labels: $labels }) {
issue {
id
labels(first: 100) {
nodes {
name
}
}
}
}
}`,
{ issueId: issueNodeId, labels: labelIntentUpdates, headers: { "GraphQL-Features": "update_issue_suggestions" } }
),
RATE_LIMIT_RETRY_CONFIG,
`add_labels to ${contextType} #${itemNumber} in ${itemRepo}`
);

core.info(`Successfully added ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo}`);
const afterLabels = result?.updateIssue?.issue?.labels?.nodes || [];
return attachExecutionState(
{
success: true,
number: itemNumber,
repo: itemRepo,
labelsAdded: uniqueLabels,
contextType,
},
beforeState,
{
...beforeState,
labels: normalizeLabelNames(afterLabels),
}
);
}

const { data: labels } = await withRetry(
() =>
githubClient.rest.issues.addLabels({
Expand Down
2 changes: 1 addition & 1 deletion setup/js/check_workflow_recompile_needed.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const { getErrorMessage } = require("./error_helpers.cjs");
const { getFooterWorkflowRecompileMessage, getFooterWorkflowRecompileCommentMessage, generateXMLMarker, getDetectionCautionAlert } = require("./messages_footer.cjs");
const fs = require("fs");
const { getGitAuthEnv } = require("./git_helpers.cjs");
const { getGitAuthEnv } = require("./git_auth_helpers.cjs");
const { resolvePullRequestRepo } = require("./pr_helpers.cjs");
const { pushSignedCommits } = require("./push_signed_commits.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
Expand Down
4 changes: 3 additions & 1 deletion setup/js/close_entity_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ function buildCommentBody(body, triggeringIssueNumber, triggeringPRNumber) {
// Caller is responsible for sanitizing body before passing it here.
const detectionCaution = getDetectionCautionAlert(workflowName, runUrl);
const bodyWithCaution = detectionCaution ? detectionCaution + "\n\n" + body.trim() : body.trim();
return bodyWithCaution + getTrackerID("markdown") + generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined, undefined, { skipDetectionCaution: true });
return (
bodyWithCaution + getTrackerID("markdown") + "\n\n" + generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined, undefined, { skipDetectionCaution: true })
);
}

/**
Expand Down
29 changes: 8 additions & 21 deletions setup/js/convert_gateway_config_claude.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require("./shim.cjs");
*/

const path = require("path");
const { normalizeGatewayEntry, loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs");
const { normalizeGatewayEntry, runGatewayConversion } = require("./convert_gateway_config_shared.cjs");

const OUTPUT_PATH = path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw/mcp-config/mcp-servers.json");

Expand All @@ -43,26 +43,13 @@ function transformClaudeEntry(entry, urlPrefix) {
}

function main() {
const { gatewayOutput, domain, port, urlPrefix, cliServers, servers } = loadGatewayContext();

core.info("Converting gateway configuration to Claude format...");
core.info(`Input: ${gatewayOutput}`);
core.info(`Target domain: ${domain}:${port}`);
logCLIFilters(cliServers);
const result = filterAndTransformServers(servers, cliServers, (_name, entry) => transformClaudeEntry(entry, urlPrefix));

const output = JSON.stringify({ mcpServers: result }, null, 2);
logServerStats(servers, Object.keys(result).length);

// Write with owner-only permissions (0o600) to protect the gateway bearer token.
// An attacker who reads mcp-servers.json could bypass --allowed-tools by issuing
// raw JSON-RPC calls directly to the gateway.
writeSecureOutput(OUTPUT_PATH, output);

core.info(`Claude configuration written to ${OUTPUT_PATH}`);
core.info("");
core.info("Converted configuration:");
core.info(output);
return runGatewayConversion({
format: "Claude",
engine: "Claude",
outputPath: OUTPUT_PATH,
transformServer: (_name, entry, urlPrefix) => transformClaudeEntry(entry, urlPrefix),
serialize: servers => JSON.stringify({ mcpServers: servers }, null, 2),
});
}

if (require.main === module) {
Expand Down
59 changes: 21 additions & 38 deletions setup/js/convert_gateway_config_codex.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require("./shim.cjs");
*/

const path = require("path");
const { loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs");
const { runGatewayConversion } = require("./convert_gateway_config_shared.cjs");

const OUTPUT_PATH = path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw/mcp-config/config.toml");

Expand All @@ -47,43 +47,26 @@ function toCodexTomlSection(name, value, urlPrefix) {
}

function main() {
const { gatewayOutput, domain, port, cliServers, servers } = loadGatewayContext();

core.info("Converting gateway configuration to Codex TOML format...");
core.info(`Input: ${gatewayOutput}`);
core.info(`Target domain: ${domain}:${port}`);

// For host.docker.internal, resolve to the gateway IP to avoid DNS resolution
// issues in Rust
let resolvedDomain = domain;
if (domain === "host.docker.internal") {
// AWF network gateway IP is always 172.30.0.1
resolvedDomain = "172.30.0.1";
core.info(`Resolving host.docker.internal to gateway IP: ${resolvedDomain}`);
}

const urlPrefix = `http://${resolvedDomain}:${port}`;
logCLIFilters(cliServers);
const filteredServers = filterAndTransformServers(servers, cliServers, (_name, entry) => entry);

// Build the TOML output
let toml = '[history]\npersistence = "none"\n\n';

for (const [name, value] of Object.entries(filteredServers)) {
toml += toCodexTomlSection(name, value, urlPrefix);
}

logServerStats(servers, Object.keys(filteredServers).length);

// Write with owner-only permissions (0o600) to protect the gateway bearer token.
// An attacker who reads config.toml could issue raw JSON-RPC calls directly
// to the gateway.
writeSecureOutput(OUTPUT_PATH, toml);

core.info(`Codex configuration written to ${OUTPUT_PATH}`);
core.info("");
core.info("Converted configuration:");
core.info(toml);
return runGatewayConversion({
format: "Codex TOML",
engine: "Codex",
outputPath: OUTPUT_PATH,
getUrlPrefix: ({ domain, port }) => {
if (domain === "host.docker.internal") {
core.info("Resolving host.docker.internal to gateway IP: 172.30.0.1");
return `http://172.30.0.1:${port}`;
}
return `http://${domain}:${port}`;
},
transformServer: (_name, entry) => entry,
serialize: (servers, _context, urlPrefix) => {
let toml = '[history]\npersistence = "none"\n\n';
for (const [name, value] of Object.entries(servers)) {
toml += toCodexTomlSection(name, value, urlPrefix);
}
return toml;
},
});
}

if (require.main === module) {
Expand Down
29 changes: 8 additions & 21 deletions setup/js/convert_gateway_config_copilot.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require("./shim.cjs");
*/

const path = require("path");
const { rewriteUrl, normalizeGatewayEntry, loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs");
const { rewriteUrl, normalizeGatewayEntry, runGatewayConversion } = require("./convert_gateway_config_shared.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
Expand Down Expand Up @@ -72,26 +72,13 @@ function main() {
return;
}

const { gatewayOutput, domain, port, urlPrefix, cliServers, servers } = loadGatewayContext();

core.info("Converting gateway configuration to Copilot format...");
core.info(`Input: ${gatewayOutput}`);
core.info(`Target domain: ${domain}:${port}`);
logCLIFilters(cliServers);
const result = filterAndTransformServers(servers, cliServers, (_name, entry) => transformCopilotEntry(entry, urlPrefix));

const output = JSON.stringify({ mcpServers: result }, null, 2);
logServerStats(servers, Object.keys(result).length);

// 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(outputPath, output);

core.info(`Copilot configuration written to ${outputPath}`);
core.info("");
core.info("Converted configuration:");
core.info(output);
return runGatewayConversion({
format: "Copilot",
engine: "Copilot",
outputPath,
transformServer: (_name, entry, urlPrefix) => transformCopilotEntry(entry, urlPrefix),
serialize: servers => JSON.stringify({ mcpServers: servers }, null, 2),
});
}

if (require.main === module) {
Expand Down
Loading
Loading