Skip to content

Commit 8703f55

Browse files
isadeksbgagentkrokoko
authored
fix(ecs): write task payload to S3 instead of 8 KB containerOverrides (#502) (#503)
* fix(ecs): write task payload to S3, not inline overrides (#502) The ECS compute strategy inlined the full orchestrator payload (incl. the large hydrated_context) into the AGENT_PAYLOAD container-override env var. ECS RunTask caps the TOTAL containerOverrides blob at 8192 bytes, so any real task was rejected before the container started: InvalidParameterException: Container Overrides length must be at most 8192 AgentCore is unaffected — it passes the payload in the InvokeAgentRuntime request body, which has no comparable limit. The bug only surfaces with a realistic hydrated payload, which is why the prior ECS smoke test (a small Rust cargo-check, #494) didn't catch it. Fix — stash the payload out-of-band and pass only a pointer: - New EcsPayloadBucket construct (mirrors TraceArtifactsBucket): BLOCK_ALL, enforceSSL, S3_MANAGED encryption, 1-day lifecycle TTL (payloads are ephemeral — read once at boot). Dedicated bucket so the ECS task role's S3 read is scoped to payloads only and can't touch attachments/traces. - ecs-strategy: when ECS_PAYLOAD_BUCKET is set, PutObject the payload to <task_id>/payload.json and pass AGENT_PAYLOAD_S3_URI in the override; the boot command fetches+parses it via boto3. Inline AGENT_PAYLOAD remains as a fallback (small payloads / no bucket), so nothing regresses. deleteEcsPayload helper removes the object. - orchestrate-task finalize: best-effort deleteEcsPayload for ECS tasks once terminal (the container has long since read it); lifecycle rule is the crash backstop. - EcsAgentCluster: accept payloadBucket, inject ECS_PAYLOAD_BUCKET env, grant the task role READ ONLY (untrusted repo code must not write/delete payloads; the trusted orchestrator owns write+delete). Session-role-aware. - task-orchestrator: ecsPayloadBucket prop → grantPut + grantDelete to the orchestrator; @aws-sdk/client-s3 added to bundling externals. - agent.ts: updated the commented uncomment-to-enable ECS scaffolding to wire the payload bucket. Tests: new bucket construct (TTL/SSL/block-public/autoDelete); strategy S3-write + URI-pointer + inline fallback + deleteEcsPayload (incl. best-effort swallow + no-op without bucket); cluster read-grant + env var + read-only (no put/delete). Full build green. Closes #502 * fix(ecs): remove dead ECS_PAYLOAD_OBJECT_KEY_PREFIX export (review B1) The `export const ECS_PAYLOAD_OBJECT_KEY_PREFIX = ''` constant was referenced nowhere — ecsPayloadKey() (ecs-strategy.ts) builds `<task_id>/payload.json` independently and already documents that layout in its own docstring. The dead export was a "prefix" whose value was '' with a docstring describing a key layout it didn't produce, and it raised the knip dead-code count, tripping the ratchet inside `mise run build`. Delete it; ecsPayloadKey remains the single source of truth for the key layout. --------- Co-authored-by: bgagent <bgagent@noreply.github.com> Co-authored-by: Alain Krok <alkrok@amazon.com>
1 parent 65e96a8 commit 8703f55

9 files changed

Lines changed: 610 additions & 9 deletions

File tree

cdk/src/constructs/ecs-agent-cluster.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets';
2424
import * as ecs from 'aws-cdk-lib/aws-ecs';
2525
import * as iam from 'aws-cdk-lib/aws-iam';
2626
import * as logs from 'aws-cdk-lib/aws-logs';
27+
import * as s3 from 'aws-cdk-lib/aws-s3';
2728
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
2829
import { NagSuppressions } from 'cdk-nag';
2930
import { Construct } from 'constructs';
@@ -39,6 +40,18 @@ export interface EcsAgentClusterProps {
3940
readonly githubTokenSecret: secretsmanager.ISecret;
4041
readonly memoryId?: string;
4142

43+
/**
44+
* S3 bucket holding per-task ECS payloads (#502). The orchestrator writes the
45+
* payload (incl. the large hydrated_context, which can't fit in the 8 KB
46+
* RunTask containerOverrides limit) here and passes only an
47+
* `AGENT_PAYLOAD_S3_URI` pointer; the container fetches it on boot. The task
48+
* role gets **read-only** on this bucket — the container runs untrusted repo
49+
* code, so it must not be able to delete payloads (the trusted orchestrator
50+
* owns write + delete). When omitted (isolated construct tests / deployments
51+
* that still pass the payload inline), no grant or env var is added.
52+
*/
53+
readonly payloadBucket?: s3.IBucket;
54+
4255
/**
4356
* Per-task SessionRole (#209). When provided, tenant-data DynamoDB access
4457
* (task/events tables) is NOT granted to the Fargate task role; instead the
@@ -121,6 +134,10 @@ export class EcsAgentCluster extends Construct {
121134
LOG_GROUP_NAME: logGroup.logGroupName,
122135
GITHUB_TOKEN_SECRET_ARN: props.githubTokenSecret.secretArn,
123136
...(props.memoryId && { MEMORY_ID: props.memoryId }),
137+
// #502: the payload bucket name so the orchestrator-issued
138+
// AGENT_PAYLOAD_S3_URI can be fetched. (The orchestrator sets the URI
139+
// per-task via container override; this is informational parity.)
140+
...(props.payloadBucket && { ECS_PAYLOAD_BUCKET: props.payloadBucket.bucketName }),
124141
// Per-session IAM scoping (#209): when a SessionRole is wired, the
125142
// agent assumes it for tenant-data access (see aws_session.py).
126143
...(props.agentSessionRole && {
@@ -149,6 +166,15 @@ export class EcsAgentCluster extends Construct {
149166
// agent assumes the SessionRole — stays on the task role).
150167
props.githubTokenSecret.grantRead(taskRole);
151168

169+
// #502: read-only on the ECS payload bucket so the container can fetch its
170+
// payload (AGENT_PAYLOAD_S3_URI) at boot. READ only — the container runs
171+
// untrusted repo code, so it must not be able to write or delete payloads
172+
// (the trusted orchestrator owns write + delete). Stays on the task role
173+
// (read once at startup, before the agent assumes any SessionRole).
174+
if (props.payloadBucket) {
175+
props.payloadBucket.grantRead(taskRole);
176+
}
177+
152178
// Bedrock model invocation — scoped to explicit foundation-model and
153179
// cross-region inference-profile ARNs (parity with the AgentCore runtime
154180
// grants in agent.ts), NOT a Resource: '*' wildcard. The model set is the
@@ -192,7 +218,7 @@ export class EcsAgentCluster extends Construct {
192218
NagSuppressions.addResourceSuppressions(this.taskDefinition, [
193219
{
194220
id: 'AwsSolutions-IAM5',
195-
reason: 'DynamoDB index/* wildcards from CDK grantReadWriteData (UserConcurrencyTable, and task tables only when no SessionRole is wired); Secrets Manager wildcards from CDK grantRead; CloudWatch Logs wildcards from CDK grantWrite. Bedrock InvokeModel is scoped to explicit model/inference-profile ARNs (no wildcard resource).',
221+
reason: 'DynamoDB index/* wildcards from CDK grantReadWriteData (UserConcurrencyTable, and task tables only when no SessionRole is wired); Secrets Manager wildcards from CDK grantRead; CloudWatch Logs wildcards from CDK grantWrite; S3 object/* wildcard from CDK grantRead on the ECS payload bucket (read-only, scoped to that bucket — #502). Bedrock InvokeModel is scoped to explicit model/inference-profile ARNs (no wildcard resource).',
196222
},
197223
{
198224
id: 'AwsSolutions-ECS2',
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* MIT No Attribution
3+
*
4+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
7+
* the Software without restriction, including without limitation the rights to
8+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
* the Software, and to permit persons to whom the Software is furnished to do so.
10+
*
11+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17+
* SOFTWARE.
18+
*/
19+
20+
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
21+
import * as s3 from 'aws-cdk-lib/aws-s3';
22+
import { Construct } from 'constructs';
23+
24+
/**
25+
* Lifecycle expiry for ECS task payloads. The payload is consumed once, at
26+
* container boot, and the orchestrator deletes it promptly in the ``finalize``
27+
* step. This 1-day rule is only a crash backstop: if the orchestrator dies
28+
* before finalize (rare — it runs under durable execution), the object is still
29+
* reaped within a day instead of lingering. Payloads carry the hydrated prompt
30+
* context, so a tight TTL also keeps the blast radius of an accidental
31+
* permission leak small.
32+
*/
33+
export const ECS_PAYLOAD_TTL_DAYS = 1;
34+
35+
/**
36+
* Properties for the EcsPayloadBucket construct.
37+
*/
38+
export interface EcsPayloadBucketProps {
39+
/**
40+
* Removal policy for the bucket.
41+
* @default RemovalPolicy.DESTROY
42+
*/
43+
readonly removalPolicy?: RemovalPolicy;
44+
45+
/**
46+
* Whether to auto-delete objects when the bucket is removed (so ``cdk
47+
* destroy`` does not need a manual bucket-empty first). Mirrors
48+
* ``TraceArtifactsBucket`` / ``AttachmentsBucket``. Deploys CDK's
49+
* ``Custom::S3AutoDeleteObjects`` Lambda with delete permissions on this
50+
* bucket — acceptable here because the contents are ephemeral throwaway
51+
* payloads.
52+
* @default true
53+
*/
54+
readonly autoDeleteObjects?: boolean;
55+
}
56+
57+
/**
58+
* S3 bucket for ECS task payloads (#502).
59+
*
60+
* The ECS compute strategy cannot pass the orchestrator payload (repo URL,
61+
* prompt, and the large ``hydrated_context``) inline: a Fargate ``RunTask``
62+
* caps the entire ``containerOverrides`` blob at 8192 bytes, and the hydrated
63+
* context routinely exceeds that, so the call is rejected with
64+
* ``InvalidParameterException``. (AgentCore is unaffected — it passes the
65+
* payload in the ``InvokeAgentRuntime`` request body, which has no comparable
66+
* limit.) Instead, the orchestrator writes the payload to
67+
* ``s3://<bucket>/<task_id>/payload.json`` and passes only a small
68+
* ``AGENT_PAYLOAD_S3_URI`` pointer in the override; the container fetches and
69+
* parses it on boot.
70+
*
71+
* Dedicated (not co-tenant with attachments/traces) so the boundary is
72+
* structural: the ECS task role gets S3 **read** here and nowhere else, the
73+
* attachments feature can never collide with payload keys, and the tight
74+
* 1-day TTL is whole-bucket rather than a prefix-scoped rule grafted onto a
75+
* shared bucket.
76+
*
77+
* Security / hygiene (parity with TraceArtifactsBucket):
78+
* - ``blockPublicAccess: BLOCK_ALL`` + ``enforceSSL: true`` — no public read,
79+
* TLS-only transport.
80+
* - ``encryption: S3_MANAGED`` — server-side encryption at rest.
81+
* - 1-day lifecycle expiry — payloads are ephemeral (read once at boot,
82+
* deleted by the orchestrator at finalize); this is the crash backstop.
83+
*/
84+
export class EcsPayloadBucket extends Construct {
85+
/** The underlying S3 bucket. */
86+
public readonly bucket: s3.Bucket;
87+
88+
constructor(scope: Construct, id: string, props: EcsPayloadBucketProps = {}) {
89+
super(scope, id);
90+
91+
this.bucket = new s3.Bucket(this, 'Bucket', {
92+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
93+
encryption: s3.BucketEncryption.S3_MANAGED,
94+
enforceSSL: true,
95+
lifecycleRules: [
96+
{
97+
id: 'ecs-payload-ttl',
98+
enabled: true,
99+
expiration: Duration.days(ECS_PAYLOAD_TTL_DAYS),
100+
// Reap incomplete multipart uploads after 1 day. Object expiration
101+
// does not apply to in-flight MPUs (they are not objects yet), so a
102+
// separate reaper keeps stale upload parts from lingering.
103+
abortIncompleteMultipartUploadAfter: Duration.days(1),
104+
},
105+
],
106+
removalPolicy: props.removalPolicy ?? RemovalPolicy.DESTROY,
107+
autoDeleteObjects: props.autoDeleteObjects ?? true,
108+
});
109+
}
110+
}

cdk/src/constructs/task-orchestrator.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ export interface TaskOrchestratorProps {
159159
readonly executionRoleArn: string;
160160
};
161161

162+
/**
163+
* S3 bucket for per-task ECS payloads (#502). When provided (alongside
164+
* ``ecsConfig``), the orchestrator writes the payload here and passes only an
165+
* ``AGENT_PAYLOAD_S3_URI`` pointer in the RunTask override (the full payload
166+
* exceeds the 8 KB containerOverrides limit), then deletes the object in the
167+
* finalize step. The orchestrator gets write + delete; the ECS task role gets
168+
* read-only (granted on the bucket by ``EcsAgentCluster``).
169+
*/
170+
readonly ecsPayloadBucket?: s3.IBucket;
171+
162172
/**
163173
* S3 bucket for task attachments. When provided, the orchestrator gets
164174
* ReadWrite grants for URL fetch/screen/upload during hydration.
@@ -220,6 +230,7 @@ export class TaskOrchestrator extends Construct {
220230
'@aws-sdk/client-ecs',
221231
'@aws-sdk/client-lambda',
222232
'@aws-sdk/client-bedrock-runtime',
233+
'@aws-sdk/client-s3',
223234
'@aws-sdk/client-secrets-manager',
224235
'@aws-sdk/lib-dynamodb',
225236
'@aws-sdk/util-dynamodb',
@@ -262,6 +273,9 @@ export class TaskOrchestrator extends Construct {
262273
ECS_SECURITY_GROUP: props.ecsConfig.securityGroup,
263274
ECS_CONTAINER_NAME: props.ecsConfig.containerName,
264275
}),
276+
// #502: bucket the orchestrator writes the ECS payload to (and deletes
277+
// from at finalize); the ECS strategy reads this to build the S3 URI.
278+
...(props.ecsPayloadBucket && { ECS_PAYLOAD_BUCKET: props.ecsPayloadBucket.bucketName }),
265279
...(props.attachmentsBucket && { ATTACHMENTS_BUCKET_NAME: props.attachmentsBucket.bucketName }),
266280
},
267281
bundling: orchestratorBundling,
@@ -280,6 +294,15 @@ export class TaskOrchestrator extends Construct {
280294
props.attachmentsBucket.grantReadWrite(this.fn);
281295
}
282296

297+
// #502: ECS payload bucket — the orchestrator writes the payload before
298+
// RunTask and deletes it at finalize. Write + delete only (it never reads
299+
// its own payload back; the ECS container is the reader, with its own
300+
// read-only grant from EcsAgentCluster).
301+
if (props.ecsPayloadBucket) {
302+
props.ecsPayloadBucket.grantPut(this.fn);
303+
props.ecsPayloadBucket.grantDelete(this.fn);
304+
}
305+
283306
// Durable execution managed policy
284307
this.fn.role!.addManagedPolicy(
285308
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicDurableExecutionRolePolicy'),

cdk/src/handlers/orchestrate-task.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
type PollState,
3838
} from './shared/orchestrator';
3939
import { runPreflightChecks } from './shared/preflight';
40+
import { deleteEcsPayload } from './shared/strategies/ecs-strategy';
4041
import type { TaskRecord } from './shared/types';
4142
import { workflowIsReadOnly, workflowRequiresRepo } from './shared/workflows';
4243

@@ -296,6 +297,14 @@ const durableHandler: DurableExecutionHandler<OrchestrateTaskEvent, void> = asyn
296297
// Step 6: Finalize — update terminal status, emit events, release concurrency
297298
await context.step('finalize', async () => {
298299
await finalizeTask(taskId, finalPollState, task.user_id);
300+
// #502: the task is terminal — the container has long since read its
301+
// payload, so delete the ephemeral S3 payload object now. Best-effort
302+
// (deleteEcsPayload swallows errors) and a no-op for AgentCore tasks /
303+
// deployments without a payload bucket; the bucket's 1-day lifecycle rule
304+
// is the backstop if this delete or the whole step never runs.
305+
if (blueprintConfig.compute_type === 'ecs') {
306+
await deleteEcsPayload(taskId);
307+
}
299308
});
300309
};
301310

cdk/src/handlers/shared/strategies/ecs-strategy.ts

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import { ECSClient, RunTaskCommand, DescribeTasksCommand, StopTaskCommand } from '@aws-sdk/client-ecs';
21+
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
2122
import type { ComputeStrategy, SessionHandle, SessionStatus } from '../compute-strategy';
2223
import { logger } from '../logger';
2324
import type { BlueprintConfig } from '../repo-config';
@@ -30,11 +31,60 @@ function getClient(): ECSClient {
3031
return sharedClient;
3132
}
3233

34+
let sharedS3Client: S3Client | undefined;
35+
function getS3Client(): S3Client {
36+
if (!sharedS3Client) {
37+
sharedS3Client = new S3Client({});
38+
}
39+
return sharedS3Client;
40+
}
41+
3342
const ECS_CLUSTER_ARN = process.env.ECS_CLUSTER_ARN;
3443
const ECS_TASK_DEFINITION_ARN = process.env.ECS_TASK_DEFINITION_ARN;
3544
const ECS_SUBNETS = process.env.ECS_SUBNETS;
3645
const ECS_SECURITY_GROUP = process.env.ECS_SECURITY_GROUP;
3746
const ECS_CONTAINER_NAME = process.env.ECS_CONTAINER_NAME ?? 'AgentContainer';
47+
const ECS_PAYLOAD_BUCKET = process.env.ECS_PAYLOAD_BUCKET;
48+
49+
/**
50+
* Inline-payload size (bytes) above which we warn that RunTask will likely
51+
* reject the call when no payload bucket is configured. ECS caps the TOTAL
52+
* containerOverrides blob at 8192 bytes; the other env vars + command consume
53+
* some of that, so 6 KB of payload is the practical danger line (#502).
54+
*/
55+
const INLINE_PAYLOAD_WARN_BYTES = 6144;
56+
57+
/**
58+
* S3 object key for a task's ECS payload. One object per task under its own
59+
* task-id prefix; deleted by the orchestrator at finalize (see
60+
* ``deleteEcsPayload``), with the bucket's 1-day lifecycle rule as a backstop.
61+
*/
62+
export function ecsPayloadKey(taskId: string): string {
63+
return `${taskId}/payload.json`;
64+
}
65+
66+
/**
67+
* Delete a task's ECS payload object. Best-effort: a failed delete must never
68+
* fail the task — the bucket's 1-day lifecycle rule reaps it regardless. Called
69+
* from the orchestrator's ``finalize`` step once the task is terminal. No-ops
70+
* when the payload bucket isn't configured (AgentCore-only deployments).
71+
*/
72+
export async function deleteEcsPayload(taskId: string): Promise<void> {
73+
if (!ECS_PAYLOAD_BUCKET) return;
74+
try {
75+
await getS3Client().send(new DeleteObjectCommand({
76+
Bucket: ECS_PAYLOAD_BUCKET,
77+
Key: ecsPayloadKey(taskId),
78+
}));
79+
logger.info('Deleted ECS payload object', { task_id: taskId });
80+
} catch (err) {
81+
// Non-fatal — the lifecycle rule is the backstop.
82+
logger.warn('Failed to delete ECS payload object (non-fatal)', {
83+
task_id: taskId,
84+
error: err instanceof Error ? err.message : String(err),
85+
});
86+
}
87+
}
3888

3989
export class EcsComputeStrategy implements ComputeStrategy {
4090
readonly type = 'ecs';
@@ -63,6 +113,37 @@ export class EcsComputeStrategy implements ComputeStrategy {
63113
// This avoids the server entirely and runs the agent in batch mode.
64114
const payloadJson = JSON.stringify(payload);
65115

116+
// #502: the payload (esp. hydrated_context) routinely exceeds the 8192-byte
117+
// cap that ECS RunTask enforces on the TOTAL containerOverrides blob, which
118+
// rejected the call with InvalidParameterException. Write the payload to S3
119+
// and pass only a small pointer (AGENT_PAYLOAD_S3_URI); the container fetches
120+
// it on boot. The inline AGENT_PAYLOAD remains as a fallback for small
121+
// payloads / deployments without a payload bucket configured.
122+
let payloadS3Uri: string | undefined;
123+
if (ECS_PAYLOAD_BUCKET) {
124+
const key = ecsPayloadKey(taskId);
125+
await getS3Client().send(new PutObjectCommand({
126+
Bucket: ECS_PAYLOAD_BUCKET,
127+
Key: key,
128+
Body: payloadJson,
129+
ContentType: 'application/json',
130+
}));
131+
payloadS3Uri = `s3://${ECS_PAYLOAD_BUCKET}/${key}`;
132+
logger.info('Wrote ECS payload to S3', {
133+
task_id: taskId,
134+
bytes: payloadJson.length,
135+
uri: payloadS3Uri,
136+
});
137+
} else if (payloadJson.length > INLINE_PAYLOAD_WARN_BYTES) {
138+
// No bucket configured AND the payload is large enough that the inline
139+
// path will almost certainly blow the 8192-byte overrides cap. Surface a
140+
// clear cause rather than a raw InvalidParameterException from RunTask.
141+
logger.warn('ECS payload is large but ECS_PAYLOAD_BUCKET is not set — RunTask may reject it (see #502)', {
142+
task_id: taskId,
143+
bytes: payloadJson.length,
144+
});
145+
}
146+
66147
const containerEnv = [
67148
{ name: 'TASK_ID', value: taskId },
68149
{ name: 'REPO_URL', value: String(payload.repo_url ?? '') },
@@ -73,26 +154,35 @@ export class EcsComputeStrategy implements ComputeStrategy {
73154
...(blueprintConfig.model_id ? [{ name: 'ANTHROPIC_MODEL', value: blueprintConfig.model_id }] : []),
74155
...(blueprintConfig.system_prompt_overrides ? [{ name: 'SYSTEM_PROMPT_OVERRIDES', value: blueprintConfig.system_prompt_overrides }] : []),
75156
{ name: 'CLAUDE_CODE_USE_BEDROCK', value: '1' },
76-
// Full orchestrator payload as JSON — the Python wrapper reads this to
77-
// call run_task() with all fields including hydrated_context.
78-
{ name: 'AGENT_PAYLOAD', value: payloadJson },
157+
// #502: prefer the S3 pointer; fall back to the inline payload when no
158+
// bucket is configured (keeps small-payload / AgentCore-only deployments
159+
// working with no behavior change).
160+
...(payloadS3Uri
161+
? [{ name: 'AGENT_PAYLOAD_S3_URI', value: payloadS3Uri }]
162+
: [{ name: 'AGENT_PAYLOAD', value: payloadJson }]),
79163
...(payload.github_token_secret_arn
80164
? [{ name: 'GITHUB_TOKEN_SECRET_ARN', value: String(payload.github_token_secret_arn) }]
81165
: []),
82166
...(payload.memory_id ? [{ name: 'MEMORY_ID', value: String(payload.memory_id) }] : []),
83167
];
84168

85169
// Override the container command to run a Python one-liner that:
86-
// 1. Reads the AGENT_PAYLOAD env var (full orchestrator payload JSON)
87-
// 2. Calls entrypoint.run_task() directly with all fields
88-
// 3. Exits with code 0 on success, 1 on failure
170+
// 1. Loads the payload — from S3 (AGENT_PAYLOAD_S3_URI) when set, else the
171+
// inline AGENT_PAYLOAD env var (fallback).
172+
// 2. Calls entrypoint.run_task() directly with all fields.
173+
// 3. Exits with code 0 on success, 1 on failure.
89174
// This bypasses the uvicorn server entirely — no HTTP, no OTEL noise.
90175
const bootCommand = [
91176
'python', '-c',
92177
'import json, os, sys; '
93178
+ 'sys.path.insert(0, "/app/src"); '
94179
+ 'from entrypoint import run_task; '
95-
+ 'p = json.loads(os.environ["AGENT_PAYLOAD"]); '
180+
+ '_uri = os.environ.get("AGENT_PAYLOAD_S3_URI"); '
181+
+ 'p = ('
182+
+ 'json.loads(__import__("boto3").client("s3").get_object('
183+
+ 'Bucket=_uri.split("/",3)[2], Key=_uri.split("/",3)[3])["Body"].read()) '
184+
+ 'if _uri else json.loads(os.environ["AGENT_PAYLOAD"])'
185+
+ '); '
96186
+ 'r = run_task('
97187
+ 'repo_url=p.get("repo_url",""), '
98188
+ 'task_description=p.get("prompt",""), '

0 commit comments

Comments
 (0)