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
37 changes: 36 additions & 1 deletion setup/js/agent_execution.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,41 @@ function agentErrorDiagnosticText(content) {
return [...diagnostics, ...collectNativeErrorEvidence(errors).diagnostics].join("\n");
}

const ENGINE_ERROR_SUMMARIES = Object.freeze({
authentication_failed: "Authentication failed",
inference_access_error: "Inference access denied by policy settings",
mcp_policy_error: "MCP servers were blocked by policy",
agentic_engine_timeout: "Engine timed out",
model_not_supported_error: "Requested model is not supported",
http_400_response_error: "Provider returned HTTP 400 Bad Request",
capi_quota_exceeded_error: "Provider quota or rate limit exceeded",
capi_server_error: "Provider server error after retries",
invocation_cap_exceeded: "Maximum LLM invocations exceeded",
max_cache_misses_exceeded: "Maximum consecutive cache misses exceeded",
missing_model_pricing_error: "Model has no AI credits pricing",
shell_expansion_guard_rejected: "Shell expansion guard rejected a command",
max_ai_credits_exceeded: "Maximum AI credits exceeded",
effective_tokens_limit_exceeded: "Effective token limit exceeded",
permission_denied_limit_exceeded: "Permission denial limit exceeded",
model_policy_violation: "Model policy violation",
awf_api_proxy_blocking_requests: "AWF API proxy blocked requests",
goal_already_active: "Session already has an active goal",
sandbox_runtime_crash: "Sandbox runtime crashed",
});

/**
* Preserve actionable error classes without publishing raw log lines or payloads.
* @param {string} content
* @returns {string}
*/
function agentErrorSummaryText(content) {
const execution = collectAgentExecution({ content });
return (execution?.data.categories || [])
.filter(category => Object.hasOwn(ENGINE_ERROR_SUMMARIES, category))
.map(category => `Engine error: ${ENGINE_ERROR_SUMMARIES[category]} (${category})`)
.join("\n");
}

/**
* @param {any[]} errors
*/
Expand Down Expand Up @@ -234,4 +269,4 @@ function collectAgentExecution({ content = "", events = [], categories = [], exi
return { type: "agent.execution", data };
}

module.exports = { collectAgentExecution, agentErrorDiagnosticText, validateAgentExecution, validateAgentExitCode, parseAgentExitCode, isAgentExecutionEvent };
module.exports = { collectAgentExecution, agentErrorDiagnosticText, agentErrorSummaryText, validateAgentExecution, validateAgentExitCode, parseAgentExitCode, isAgentExecutionEvent };
35 changes: 23 additions & 12 deletions setup/js/claude_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ const {
fetchAWFReflect,
fetchModelsFromUrl,
normalizeReflectProviderName,
REFLECT_PROVIDER_ALIASES,
resolveProviderEndpointFromReflect,
} = require("./awf_reflect.cjs");
const { emitMissingToolPermissionIssue, emitInfrastructureIncomplete, hasExpectedSafeOutputs, hasNoopInSafeOutputs, hasTerminalSafeOutput } = require("./safeoutputs_cli.cjs");
const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs");
const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError, parseAICreditsExceededProxyRejection } = require("./harness_retry_guard.cjs");
const { isCrashSignalExitCode, crashSignalNameForExitCode } = require("./harness_crash_signals.cjs");
const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs");
const { applyModelFallback } = require("./model_fallback.cjs");
const { applyModelFallback, normalizeClaudeModel, normalizeClaudeModelArgs } = require("./model_fallback.cjs");
const { parseMaxAICreditsExceededFromAuditLog } = require("./ai_credits_context.cjs");

// Pattern to detect Anthropic API overload errors (HTTP 529).
Expand Down Expand Up @@ -364,19 +365,27 @@ function stripContinueArgs(args) {

/**
* Build Claude child process env with provider endpoint overrides resolved from /reflect.
* @param {import("./awf_reflect.cjs").ReflectData | null | undefined} reflectData
* @param {NodeJS.ProcessEnv} [env]
* @param {(message: string) => void} [logger]
* @returns {Promise<NodeJS.ProcessEnv>}
*/
async function buildClaudeChildEnv(reflectData) {
const childEnv = { ...process.env };
async function buildClaudeChildEnv(reflectData, env = process.env, logger = log) {
const childEnv = { ...env };
applyClaudeRuntimeTimeouts(childEnv);
applyModelFallback(childEnv, "ANTHROPIC_MODEL", log);
const provider = normalizeReflectProviderName(process.env.GH_AW_LLM_PROVIDER, "anthropic");
if (reflectData) {
const resolved = resolveProviderEndpointFromReflect({ provider, reflectData, logger: log });
if (resolved && resolved.baseUrl) {
childEnv.ANTHROPIC_BASE_URL = resolved.baseUrl;
log(`configured ANTHROPIC_BASE_URL from /reflect for provider=${provider}: ${resolved.baseUrl}`);
}
applyModelFallback(childEnv, "ANTHROPIC_MODEL", logger);
const provider = normalizeReflectProviderName(env.GH_AW_LLM_PROVIDER, "anthropic");
const copilotProvider = REFLECT_PROVIDER_ALIASES.github.has(provider);
if (childEnv.ANTHROPIC_MODEL) {
childEnv.ANTHROPIC_MODEL = normalizeClaudeModel(childEnv.ANTHROPIC_MODEL, provider, childEnv);
}
const resolved = reflectData ? resolveProviderEndpointFromReflect({ provider: copilotProvider ? "github" : provider, reflectData, logger }) : null;
if (copilotProvider && (!resolved || !REFLECT_PROVIDER_ALIASES.github.has(normalizeReflectProviderName(resolved.endpointProvider)))) {
throw new Error("Claude GitHub Copilot inference requires a configured Copilot endpoint from AWF /reflect; keep the agent sandbox enabled and configure permissions.copilot-requests: write or COPILOT_GITHUB_TOKEN");
}
if (resolved?.baseUrl) {
childEnv.ANTHROPIC_BASE_URL = resolved.baseUrl;
logger(`configured ANTHROPIC_BASE_URL from /reflect for provider=${provider}: ${resolved.baseUrl}`);
}
return childEnv;
}
Expand Down Expand Up @@ -405,7 +414,8 @@ async function main() {
try {
const resolved = resolveClaudePromptFileArgs(args);
prompt = resolved.prompt;
const capabilities = claudeBareCapabilities(claudeRepositoryEditPolicy(stripContinueArgs(resolved.args), process.env));
const modelArgs = normalizeClaudeModelArgs(resolved.args, normalizeReflectProviderName(process.env.GH_AW_LLM_PROVIDER, "anthropic"), process.env);
const capabilities = claudeBareCapabilities(claudeRepositoryEditPolicy(stripContinueArgs(modelArgs), process.env));
initialArgs = capabilities.args;
pluginDir = capabilities.pluginDir;
} catch (err) {
Expand Down Expand Up @@ -752,6 +762,7 @@ if (typeof module !== "undefined" && module.exports) {
resolveRetryConfig,
resolveStartupRetryLimit,
applyModelFallback,
buildClaudeChildEnv,
};
}

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

const { collectArtifactSecretValues, redactManifestValue } = require("./safe_output_manifest.cjs");
const { applyAddMaskRedaction } = require("./add_mask_redaction.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");

/**
* @typedef {Object} DiagnosticOptions
* @property {string[]} [secrets]
* @property {string[]} [maskedValues]
* @property {number} [maxLength]
*/

/**
* Exact masks must run before patterns that can consume only a secret's prefix.
* @param {string} text
* @param {DiagnosticOptions} [options]
* @returns {string}
*/
function redactDiagnosticText(text, { secrets = collectArtifactSecretValues(), maskedValues = [] } = {}) {
const masks = [...secrets, ...maskedValues].sort((a, b) => b.length - a.length);
return String(redactManifestValue(applyAddMaskRedaction(text, masks), []));
}

/**
* Bound and fully normalize diagnostics before callers choose Markdown fences.
* @param {string} text
* @param {DiagnosticOptions} [options]
* @returns {string}
*/
function redactAndBoundDiagnostics(text, options = {}) {
const maxLength = options.maxLength ?? 8000;
const normalize = value => {
// Entity decoding and control/comment removal can expose another encoded layer.
// Stabilize the bounded text so later sanitization cannot create a new fence.
for (;;) {
const sanitized = sanitizeContent(value, { maxLength });
if (sanitized === value) return sanitized;
value = sanitized;
}
};
const normalized = normalize(redactDiagnosticText(text, options));
return normalize(redactDiagnosticText(normalized, options));
}

module.exports = { redactDiagnosticText, redactAndBoundDiagnostics };
16 changes: 9 additions & 7 deletions setup/js/empty_output_outcome.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
const fs = require("fs");
const path = require("path");
const { collectUnifiedSession } = require("./unified_session.cjs");
const { agentErrorDiagnosticText } = require("./agent_execution.cjs");
const { agentErrorDiagnosticText, agentErrorSummaryText } = require("./agent_execution.cjs");
const { extractDeniedCommands } = require("./permission_denied_helpers.cjs");
const { extractShellCommandFromToolData } = require("./tool_call_details.cjs");
const { collectArtifactSecretValues, redactManifestValue } = require("./safe_output_manifest.cjs");
const { collectAddMaskedValues, redactMaskedValues } = require("./add_mask_redaction.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { collectArtifactSecretValues } = require("./safe_output_manifest.cjs");
const { collectAddMaskedValues } = require("./add_mask_redaction.cjs");
const { redactDiagnosticText, redactAndBoundDiagnostics } = require("./diagnostic_sanitization.cjs");

// Keep cause titles and the category allow-list in sync; schema and docs list these causes too.
const EMPTY_OUTPUT_CAUSES = Object.freeze({
Expand Down Expand Up @@ -45,7 +45,7 @@ function buildEmptyOutputOutcome(errors, rootDir = "/tmp/gh-aw") {
}
maskedValues.push(...collectAddMaskedValues(stdio));
const secrets = collectArtifactSecretValues();
const redact = value => redactMaskedValues(String(redactManifestValue(value, secrets)), maskedValues);
const redact = value => redactDiagnosticText(String(value), { secrets, maskedValues });
const redactJson = value => JSON.stringify(value, (_key, nested) => (typeof nested === "string" ? redact(nested) : nested));
let safeoutputsCliError = false;
try {
Expand Down Expand Up @@ -117,11 +117,13 @@ function buildEmptyOutputOutcome(errors, rootDir = "/tmp/gh-aw") {
}
})
.join("\n");
// Extract denied commands without copying harness configuration or transcript data.
const attributedDiagnostics = agentErrorDiagnosticText(safeStdio);
for (const command of extractDeniedCommands(attributedDiagnostics)) diagnostics.add(`Permission denied: ${command}`);
if (attributedDiagnostics) diagnostics.add(attributedDiagnostics);
const engineSummary = agentErrorSummaryText(safeStdio);
if (engineSummary) diagnostics.add(engineSummary);
const details = [...diagnostics].slice(0, 20).join("\n");
const sanitized = sanitizeContent(redact(details), { maxLength: 8000 });
const sanitized = redactAndBoundDiagnostics(details, { secrets, maskedValues });
return {
type: "report_incomplete",
reason,
Expand Down
4 changes: 4 additions & 0 deletions setup/js/generate_aw_info.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ async function main(core, ctx, githubClient) {
event_name: ctx.eventName,
target_repo: process.env.GH_AW_INFO_TARGET_REPO || "",
staged: process.env.GH_AW_INFO_STAGED === "true",
dry_run: process.env.GH_AW_INFO_DRY_RUN === "true",
allowed_domains: allowedDomains,
firewall_enabled: process.env.GH_AW_INFO_FIREWALL_ENABLED === "true",
awf_version: process.env.GH_AW_INFO_AWF_VERSION || "",
Expand All @@ -98,6 +99,9 @@ async function main(core, ctx, githubClient) {
},
created_at: new Date().toISOString(),
};
if (awInfo.dry_run) {
core.warning("Dry-run disables compiler-managed GitHub mutations only. Custom scripts/jobs, agent shell commands, external MCP servers, and custom credentials are not verified read-only.");
}

if (process.env.GH_AW_INFO_FETCH_RUN_CREATED_AT === "true") {
try {
Expand Down
Loading
Loading