Skip to content
Open
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
10 changes: 5 additions & 5 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ description = "Test fixture signing secret in Slack verification unit test (not
stopwords = ["test-signing-secret-abc123"]

[[allowlists]]
# Test idempotency-key fixture in orchestration-release.test.ts (not a real
# credential). Suppressed by stopword rather than a .gitleaksignore commit-SHA
# fingerprint: the finding lives in history and SHA fingerprints break whenever
# history is rewritten (rebases / dep-bump merges), which is exactly how #530's
# baseline regressed. A stopword is SHA-independent. See #537 (regression of #530).
# Test idempotency-key fixture in orchestration tests (not a real credential).
# Suppressed by stopword rather than a .gitleaksignore commit-SHA fingerprint:
# the finding lives in history and SHA fingerprints break whenever history is
# rewritten (rebases / dep-bump merges), which is exactly how #530's baseline
# regressed. A stopword is SHA-independent. See #537 (regression of #530).
description = "Test idempotency-key fixture 'orch_abc_SUB-1' (not a real credential)."
stopwords = ["orch_abc_SUB-1"]

Expand Down
2 changes: 1 addition & 1 deletion agent/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ def get_config() -> TaskConfig:
issue_number=os.environ.get("ISSUE_NUMBER", ""),
github_token=os.environ.get("GITHUB_TOKEN", ""),
anthropic_model=os.environ.get("ANTHROPIC_MODEL", ""),
max_turns=int(os.environ.get("MAX_TURNS", "200")),
max_turns=int(os.environ.get("MAX_TURNS", "100")),
max_budget_usd=float(os.environ.get("MAX_BUDGET_USD", "0")) or None,
aws_region=os.environ.get("AWS_REGION", ""),
dry_run=os.environ.get("DRY_RUN", "").lower() in ("1", "true", "yes"),
Expand Down
2 changes: 1 addition & 1 deletion agent/src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ def _extract_invocation_params(inp: dict, request: Request) -> dict:
# #1: per-repo build/lint verification commands. Empty → agent defaults to mise.
build_command = inp.get("build_command", "")
lint_command = inp.get("lint_command", "")
max_turns = int(inp.get("max_turns", 0)) or int(os.environ.get("MAX_TURNS", "200"))
max_turns = int(inp.get("max_turns", 0)) or int(os.environ.get("MAX_TURNS", "100"))
max_budget_usd = float(inp.get("max_budget_usd", 0)) or None
aws_region = inp.get("aws_region") or os.environ.get("AWS_REGION", "")
task_id = inp.get("task_id", "")
Expand Down
40 changes: 40 additions & 0 deletions cdk/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ description = "Jest tests"
depends = [":compile"]
run = ["mkdir -p $TMPDIR", "yarn test"]

# Focused, low-footprint test run for iterating on one file/pattern:
# mise //cdk:testf -- orchestration-release
# Skips coverage (the heaviest phase) and runs a single worker, so it
# won't spawn the worker fleet that OOMs the Mac on the 1240-test
# stack-synth suite. Use //cdk:test for the full coverage run (CI parity).
[tasks.testf]
description = "Focused jest run (no coverage, single worker)"
run = "yarn jest --coverage=false --runInBand"

[tasks.synth]
description = "cdk synth"
run = ["mkdir -p $TMPDIR", "yarn synth"]
Expand All @@ -36,8 +45,39 @@ description = "cdk synth (quiet)"
depends = [":compile"]
run = ["mkdir -p $TMPDIR", "yarn synth:quiet"]

# Reclaim regenerable build artifacts that otherwise grow unbounded and fill the
# disk (observed in practice: a deploy died with ENOSPC — the uv cache had grown
# to 51G and Docker.raw to 112G). ALWAYS does the cheap, safe, Docker-free cleanups (stale
# cdk.out + $TMPDIR, both fully regenerated by the next build). The expensive
# uv/Docker prunes are GATED on low free disk (< MIN_FREE_GB) — running them on
# every deploy is both wasteful and risky (concurrent/looping `docker prune`
# helped wedge the daemon once). Gated prunes are best-effort (|| true)
# and run sequentially so they never pile up. Standalone: `mise //cdk:clean:disk`
# (set MIN_FREE_GB=999 to force a prune); also runs before `mise //cdk:deploy`.
[tasks."clean:disk"]
description = "Reclaim disk: stale cdk.out/$TMPDIR always; uv+docker prune only when free disk is low"
run = '''
rm -rf cdk.out || true
rm -rf "$TMPDIR"/* 2>/dev/null || true
MIN_FREE_GB="${MIN_FREE_GB:-25}"
FREE_GB=$(df -g / | awk 'NR==2 {print $4}')
echo "clean:disk — ${FREE_GB}G free (threshold ${MIN_FREE_GB}G)"
if [ "${FREE_GB:-999}" -lt "$MIN_FREE_GB" ]; then
echo "clean:disk — low disk, pruning uv + docker caches…"
uv cache prune || true
docker image prune -f || true
docker builder prune -f || true
df -h / | tail -1
else
echo "clean:disk — enough free disk, skipping uv/docker prune"
fi
'''

[tasks.deploy]
description = "cdk deploy (pass args after --)"
# Reclaim disk first — the agent-image Docker build + CDK asset bundling need
# several GB of working space, and uv/Docker caches accumulate across runs.
depends = [":clean:disk"]
run = "npx cdk deploy"

# Bootstraps with ComputeTypes=agentcore (the template default). To ALSO enable the
Expand Down
4 changes: 3 additions & 1 deletion cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"compile": "tsc --build tsconfig.json",
"watch": "tsc --build -w tsconfig.json",
"test": "jest",
"test": "jest --maxWorkers=${JEST_MAX_WORKERS:-25%}",
"eslint": "eslint --fix src test",
"synth": "npx cdk synth",
"synth:quiet": "npx cdk synth -q"
Expand Down Expand Up @@ -77,6 +77,8 @@
"<rootDir>/@(src|test)/**/*(*.)@(spec|test).ts?(x)",
"<rootDir>/@(src|test)/**/__tests__/**/*.ts?(x)"
],
"maxWorkers": "25%",
"workerIdleMemoryLimit": "1536MB",
"clearMocks": true,
"collectCoverage": true,
"coverageReporters": [
Expand Down
2 changes: 1 addition & 1 deletion cdk/src/constructs/bedrock-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Node } from 'constructs';
* runtime may invoke. Both grant sites — the AgentCore runtime in
* `stacks/agent.ts` and the ECS task role in `constructs/ecs-agent-cluster.ts`
* — derive their `grantInvoke` / IAM ARNs from this one list, so the two
* backends can never drift (they were previously two hand-synced arrays; #433).
* backends can never drift (they were previously two hand-synced arrays).
*
* Scoping is intentionally per-model (explicit foundation-model +
* cross-Region inference-profile ARNs), NOT a `Resource: '*'` wildcard — that
Expand Down
33 changes: 33 additions & 0 deletions cdk/src/constructs/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ export interface BlueprintProps {
* Override the default poll interval (ms) for awaiting agent completion.
*/
readonly pollIntervalMs?: number;

/**
* Command the agent runs to BUILD/verify the repo before opening a PR
* (and as the pre-change baseline). Drives build-regression gating: if
* the repo built green before the agent's change and fails after, the
* task fails. Defaults to ``mise run build`` when unset.
*
* Set this for repos that do NOT use mise (e.g. ``'npm run build'``,
* ``'gradle build'``, ``'make'``). Without a runnable build command,
* build-regression gating is INERT — a change that breaks the build
* still reports success (the agent emits a one-time warning on the PR).
* Runs in the agent's cloud container against the cloned repo; this is a
* compile/test verification, NOT a deployment.
*/
readonly buildCommand?: string;

/**
* Command the agent runs to LINT the repo (advisory gate). Defaults to
* ``mise run lint`` when unset. Same semantics as ``buildCommand``.
*/
readonly lintCommand?: string;
};

/**
Expand Down Expand Up @@ -239,6 +260,12 @@ export class Blueprint extends Construct {
if (props.pipeline?.pollIntervalMs !== undefined) {
item.poll_interval_ms = { N: String(props.pipeline.pollIntervalMs) };
}
if (props.pipeline?.buildCommand) {
item.build_command = { S: props.pipeline.buildCommand };
}
if (props.pipeline?.lintCommand) {
item.lint_command = { S: props.pipeline.lintCommand };
}
if (this.egressAllowlist.length > 0) {
item.egress_allowlist = { L: this.egressAllowlist.map(d => ({ S: d })) };
}
Expand Down Expand Up @@ -317,6 +344,8 @@ export class Blueprint extends Construct {
if (props.agent?.systemPromptOverrides) fields.push(', #system_prompt_overrides = :system_prompt_overrides');
if (props.credentials?.githubTokenSecretArn) fields.push(', #github_token_secret_arn = :github_token_secret_arn');
if (props.pipeline?.pollIntervalMs !== undefined) fields.push(', #poll_interval_ms = :poll_interval_ms');
if (props.pipeline?.buildCommand) fields.push(', #build_command = :build_command');
if (props.pipeline?.lintCommand) fields.push(', #lint_command = :lint_command');
if (this.egressAllowlist.length > 0) fields.push(', #egress_allowlist = :egress_allowlist');
if (this.cedarPolicies.length > 0) fields.push(', #cedar_policies = :cedar_policies');
if (this.approvalGateCap !== undefined) fields.push(', #approval_gate_cap = :approval_gate_cap');
Expand All @@ -332,6 +361,8 @@ export class Blueprint extends Construct {
if (props.agent?.systemPromptOverrides) names['#system_prompt_overrides'] = 'system_prompt_overrides';
if (props.credentials?.githubTokenSecretArn) names['#github_token_secret_arn'] = 'github_token_secret_arn';
if (props.pipeline?.pollIntervalMs !== undefined) names['#poll_interval_ms'] = 'poll_interval_ms';
if (props.pipeline?.buildCommand) names['#build_command'] = 'build_command';
if (props.pipeline?.lintCommand) names['#lint_command'] = 'lint_command';
if (this.egressAllowlist.length > 0) names['#egress_allowlist'] = 'egress_allowlist';
if (this.cedarPolicies.length > 0) names['#cedar_policies'] = 'cedar_policies';
if (this.approvalGateCap !== undefined) names['#approval_gate_cap'] = 'approval_gate_cap';
Expand All @@ -347,6 +378,8 @@ export class Blueprint extends Construct {
if (props.agent?.systemPromptOverrides) values[':system_prompt_overrides'] = { S: props.agent.systemPromptOverrides };
if (props.credentials?.githubTokenSecretArn) values[':github_token_secret_arn'] = { S: props.credentials.githubTokenSecretArn };
if (props.pipeline?.pollIntervalMs !== undefined) values[':poll_interval_ms'] = { N: String(props.pipeline.pollIntervalMs) };
if (props.pipeline?.buildCommand) values[':build_command'] = { S: props.pipeline.buildCommand };
if (props.pipeline?.lintCommand) values[':lint_command'] = { S: props.pipeline.lintCommand };
if (this.egressAllowlist.length > 0) values[':egress_allowlist'] = { L: this.egressAllowlist.map(d => ({ S: d })) };
if (this.cedarPolicies.length > 0) values[':cedar_policies'] = { L: this.cedarPolicies.map(p => ({ S: p })) };
if (this.approvalGateCap !== undefined) values[':approval_gate_cap'] = { N: String(this.approvalGateCap) };
Expand Down
12 changes: 11 additions & 1 deletion cdk/src/constructs/jira-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ export class JiraIntegration extends Construct {
const commonBundling: lambda.BundlingOptions = {
externalModules: ['@aws-sdk/*'],
};
// pdf-parse (v2, pdfjs-based) can't be esbuild-bundled — its pdfjs/native
// (@napi-rs/canvas) deps break at import (`DOMMatrix is not defined`,
// Ship it unbundled via `nodeModules` so it resolves natively at
// runtime. Mirrors TaskApi's attachment-screening bundling. Jira's #619
// attachment path screens PDFs, so its webhook processor needs the carve-out.
const attachmentScreeningBundling: lambda.BundlingOptions = {
...commonBundling,
nodeModules: ['pdf-parse'],
};

// --- Task creation environment (matches LinearIntegration / SlackIntegration pattern) ---
const createTaskEnv: Record<string, string> = {
Expand Down Expand Up @@ -258,7 +267,8 @@ export class JiraIntegration extends Construct {
JIRA_USER_MAPPING_TABLE_NAME: this.userMappingTable.tableName,
JIRA_WORKSPACE_REGISTRY_TABLE_NAME: this.workspaceRegistryTable.tableName,
},
bundling: commonBundling,
// Uses the PDF attachment-screening path (#619) — pdf-parse must stay unbundled.
bundling: attachmentScreeningBundling,
});
this.projectMappingTable.grantReadData(webhookProcessorFn);
this.userMappingTable.grantReadData(webhookProcessorFn);
Expand Down
Loading
Loading