11"""Invoke Bedrock AgentCore Harness to review a GitHub PR.
22
33Reads PR_URL from the environment. Streams harness output to stdout.
4- Uses the boto3 bedrock-agentcore client's invoke_harness API .
4+ Uses raw HTTP with SigV4 signing — no custom service model needed .
55"""
66
77import json
1111import uuid
1212
1313import boto3
14+ from botocore .auth import SigV4Auth
15+ from botocore .awsrequest import AWSRequest
16+ from botocore .eventstream import EventStreamBuffer
17+ from urllib .parse import quote
18+ import urllib3
1419
1520# ANSI color codes
1621CYAN = "\033 [36m"
2025DIM = "\033 [2m"
2126RESET = "\033 [0m"
2227
23- SCRIPTS_DIR = os .path .dirname (__file__ )
28+ SCRIPTS_DIR = os .path .join ( os . path . dirname (__file__ ), ".." )
2429
2530
2631def read_prompt (filename ):
@@ -30,37 +35,50 @@ def read_prompt(filename):
3035 return f .read ()
3136
3237
33- def invoke_harness_streaming (harness_arn , session_id , system_prompt , messages , model_id , region ):
34- """Call invoke_harness via boto3 and return the event stream."""
35- client = boto3 .client ("bedrock-agentcore" , region_name = region )
36- response = client .invoke_harness (
37- harnessArn = harness_arn ,
38- runtimeSessionId = session_id ,
39- systemPrompt = [{"text" : system_prompt }],
40- messages = messages ,
41- model = {"bedrockModelConfig" : {"modelId" : model_id }},
38+ def invoke_harness (harness_arn , body , region ):
39+ """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response.
40+
41+ InvokeHarness is not in standard boto3, so we call the REST API directly.
42+ boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.)
43+ and sign the request with SigV4. The response is an AWS binary event stream.
44+ """
45+ session = boto3 .Session (region_name = region )
46+ credentials = session .get_credentials ().get_frozen_credentials ()
47+ url = f"https://bedrock-agentcore.{ region } .amazonaws.com/harnesses/invoke?harnessArn={ quote (harness_arn , safe = '' )} "
48+ request = AWSRequest (method = "POST" , url = url , data = body , headers = {
49+ "Content-Type" : "application/json" ,
50+ "Accept" : "application/vnd.amazon.eventstream" ,
51+ })
52+ SigV4Auth (credentials , "bedrock-agentcore" , region ).add_auth (request )
53+ return urllib3 .PoolManager ().urlopen (
54+ "POST" , url , body = body ,
55+ headers = dict (request .headers ),
56+ preload_content = False ,
57+ timeout = urllib3 .Timeout (connect = 10 , read = 600 ),
4258 )
43- return response ["stream" ]
44-
45-
46- def parse_events (event_stream ):
47- """Yield (event_type, payload) tuples from the boto3 event stream."""
48- for event in event_stream :
49- if "contentBlockStart" in event :
50- yield "contentBlockStart" , event ["contentBlockStart" ]
51- elif "contentBlockDelta" in event :
52- yield "contentBlockDelta" , event ["contentBlockDelta" ]
53- elif "contentBlockStop" in event :
54- yield "contentBlockStop" , event ["contentBlockStop" ]
55- elif "messageStop" in event :
56- yield "messageStop" , event ["messageStop" ]
57- elif "internalServerException" in event :
58- yield "internalServerException" , event ["internalServerException" ]
59- elif "runtimeClientError" in event :
60- yield "runtimeClientError" , event ["runtimeClientError" ]
61-
62-
63- def print_stream (event_stream ):
59+
60+
61+ def parse_events (http_response ):
62+ """Yield (event_type, payload) tuples from the harness binary event stream.
63+
64+ The response arrives as raw bytes in AWS binary event stream format.
65+ EventStreamBuffer reassembles complete events from the 4KB chunks,
66+ and we decode each event's JSON payload before yielding it.
67+ """
68+ event_buffer = EventStreamBuffer ()
69+ for chunk in http_response .stream (4096 ):
70+ event_buffer .add_data (chunk )
71+ for event in event_buffer :
72+ if event .headers .get (":message-type" ) == "exception" :
73+ payload = json .loads (event .payload .decode ("utf-8" ))
74+ print (f"\n { RED } ERROR: { payload } { RESET } " , file = sys .stderr )
75+ sys .exit (1 )
76+ event_type = event .headers .get (":event-type" , "" )
77+ if event .payload :
78+ yield event_type , json .loads (event .payload .decode ("utf-8" ))
79+
80+
81+ def print_stream (http_response ):
6482 """Display harness events with GitHub Actions log groups.
6583
6684 The harness streams events as the agent works:
@@ -94,7 +112,7 @@ def flush_text():
94112 print (f"{ DIM } { line } { RESET } " , flush = True )
95113 text_buffer = ""
96114
97- for event_type , payload in parse_events (event_stream ):
115+ for event_type , payload in parse_events (http_response ):
98116
99117 if event_type == "contentBlockStart" :
100118 start = payload .get ("start" , {})
@@ -153,11 +171,6 @@ def flush_text():
153171 print (f"\n { RED } ERROR: { payload } { RESET } " , file = sys .stderr )
154172 sys .exit (1 )
155173
156- elif event_type == "runtimeClientError" :
157- close_group ()
158- print (f"\n { RED } ERROR: { payload .get ('message' , payload )} { RESET } " , file = sys .stderr )
159- sys .exit (1 )
160-
161174 close_group ()
162175 total = time .time () - start_time
163176 print (f"\n { GREEN } Review complete.{ RESET } { DIM } ({ iteration } tool calls, { int (total )} s total){ RESET } " )
@@ -187,10 +200,18 @@ def flush_text():
187200SYSTEM_PROMPT = read_prompt ("system.md" )
188201REVIEW_PROMPT = read_prompt ("review.md" ).format (pr_url = PR_URL )
189202
190- messages = [{"role" : "user" , "content" : [{"text" : REVIEW_PROMPT }]}]
203+ request_body = json .dumps ({
204+ "runtimeSessionId" : SESSION_ID ,
205+ "systemPrompt" : [{"text" : SYSTEM_PROMPT }],
206+ "messages" : [{"role" : "user" , "content" : [{"text" : REVIEW_PROMPT }]}],
207+ "model" : {"bedrockModelConfig" : {"modelId" : MODEL_ID }},
208+ })
209+
210+ http_response = invoke_harness (HARNESS_ARN , request_body , REGION )
191211
192- event_stream = invoke_harness_streaming (
193- HARNESS_ARN , SESSION_ID , SYSTEM_PROMPT , messages , MODEL_ID , REGION
194- )
212+ if http_response .status != 200 :
213+ error = http_response .read ().decode ("utf-8" )
214+ print (f"{ RED } ERROR: HTTP { http_response .status } : { error } { RESET } " , file = sys .stderr )
215+ sys .exit (1 )
195216
196- print_stream (event_stream )
217+ print_stream (http_response )
0 commit comments