From b8a90c90a2b287227e5edd9432912a7fbd07d638 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 30 Apr 2026 11:03:30 -0400 Subject: [PATCH 1/2] Revert "refactor: move harness resources to .github/harness/ (#992)" This reverts commit aef3890e460a9a06db7f8465a157588bc4b0f7b3. --- .github/harness/Dockerfile | 33 ------ .github/harness/README.md | 39 ------- .../{harness => scripts}/prompts/review.md | 0 .../{harness => scripts}/prompts/system.md | 0 .../python}/harness_review.py | 107 +++++++++++------- .github/workflows/pr-ai-review.yml | 2 +- .prettierignore | 2 +- 7 files changed, 66 insertions(+), 117 deletions(-) delete mode 100644 .github/harness/Dockerfile delete mode 100644 .github/harness/README.md rename .github/{harness => scripts}/prompts/review.md (100%) rename .github/{harness => scripts}/prompts/system.md (100%) rename .github/{harness => scripts/python}/harness_review.py (62%) diff --git a/.github/harness/Dockerfile b/.github/harness/Dockerfile deleted file mode 100644 index 3deec1a46..000000000 --- a/.github/harness/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - git \ - curl \ - jq \ - && rm -rf /var/lib/apt/lists/* - -# Install GitHub CLI -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - > /etc/apt/sources.list.d/github-cli.list \ - && apt-get update \ - && apt-get install -y gh \ - && rm -rf /var/lib/apt/lists/* - -# Tokens are baked into the image at build time. This image must be treated as a -# secret and stored only in a registry with equivalent access controls. -ARG CLONE_TOKEN -ARG GITHUB_TOKEN - -# Configure git to use clone token for HTTPS clones -RUN git config --global url."https://${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" - -# Persist gh CLI auth so GITHUB_TOKEN doesn't need to be in the environment -RUN mkdir -p /root/.config/gh \ - && echo "github.com:" > /root/.config/gh/hosts.yml \ - && echo " oauth_token: ${GITHUB_TOKEN}" >> /root/.config/gh/hosts.yml \ - && echo " user: agentcore-cli-automation" >> /root/.config/gh/hosts.yml \ - && echo " git_protocol: https" >> /root/.config/gh/hosts.yml - -WORKDIR /opt/workspace diff --git a/.github/harness/README.md b/.github/harness/README.md deleted file mode 100644 index d9ba15c61..000000000 --- a/.github/harness/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Harness Resources - -Container and scripts for AI-powered automation via -[AgentCore Harness](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html). - -## Structure - -``` -harness/ -├── Dockerfile # Container image for the harness runtime -├── harness_review.py # Invokes the harness to review PRs (SigV4 + event stream) -└── prompts/ - ├── system.md # System prompt (workspace context) - └── review.md # PR review task prompt -``` - -## Current: PR Reviewer - -Reviews pull requests on open/reopen via `.github/workflows/pr-ai-review.yml`. - -### Dual-token setup - -The Dockerfile takes two build args: - -- **`CLONE_TOKEN`** — baked into git config for cloning private repos -- **`GITHUB_TOKEN`** — baked into `gh` CLI auth for posting PR comments - -### Building the container - -```bash -finch build \ - --build-arg CLONE_TOKEN= \ - --build-arg GITHUB_TOKEN= \ - -t pr-reviewer .github/harness/ -``` - -## Future: Tester - -This directory will also house a harness-based test runner. diff --git a/.github/harness/prompts/review.md b/.github/scripts/prompts/review.md similarity index 100% rename from .github/harness/prompts/review.md rename to .github/scripts/prompts/review.md diff --git a/.github/harness/prompts/system.md b/.github/scripts/prompts/system.md similarity index 100% rename from .github/harness/prompts/system.md rename to .github/scripts/prompts/system.md diff --git a/.github/harness/harness_review.py b/.github/scripts/python/harness_review.py similarity index 62% rename from .github/harness/harness_review.py rename to .github/scripts/python/harness_review.py index 455fb6a3d..fbfd0b0f9 100644 --- a/.github/harness/harness_review.py +++ b/.github/scripts/python/harness_review.py @@ -1,7 +1,7 @@ """Invoke Bedrock AgentCore Harness to review a GitHub PR. Reads PR_URL from the environment. Streams harness output to stdout. -Uses the boto3 bedrock-agentcore client's invoke_harness API. +Uses raw HTTP with SigV4 signing — no custom service model needed. """ import json @@ -11,6 +11,11 @@ import uuid import boto3 +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest +from botocore.eventstream import EventStreamBuffer +from urllib.parse import quote +import urllib3 # ANSI color codes CYAN = "\033[36m" @@ -20,7 +25,7 @@ DIM = "\033[2m" RESET = "\033[0m" -SCRIPTS_DIR = os.path.dirname(__file__) +SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..") def read_prompt(filename): @@ -30,37 +35,50 @@ def read_prompt(filename): return f.read() -def invoke_harness_streaming(harness_arn, session_id, system_prompt, messages, model_id, region): - """Call invoke_harness via boto3 and return the event stream.""" - client = boto3.client("bedrock-agentcore", region_name=region) - response = client.invoke_harness( - harnessArn=harness_arn, - runtimeSessionId=session_id, - systemPrompt=[{"text": system_prompt}], - messages=messages, - model={"bedrockModelConfig": {"modelId": model_id}}, +def invoke_harness(harness_arn, body, region): + """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response. + + InvokeHarness is not in standard boto3, so we call the REST API directly. + boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.) + and sign the request with SigV4. The response is an AWS binary event stream. + """ + session = boto3.Session(region_name=region) + credentials = session.get_credentials().get_frozen_credentials() + url = f"https://bedrock-agentcore.{region}.amazonaws.com/harnesses/invoke?harnessArn={quote(harness_arn, safe='')}" + request = AWSRequest(method="POST", url=url, data=body, headers={ + "Content-Type": "application/json", + "Accept": "application/vnd.amazon.eventstream", + }) + SigV4Auth(credentials, "bedrock-agentcore", region).add_auth(request) + return urllib3.PoolManager().urlopen( + "POST", url, body=body, + headers=dict(request.headers), + preload_content=False, + timeout=urllib3.Timeout(connect=10, read=600), ) - return response["stream"] - - -def parse_events(event_stream): - """Yield (event_type, payload) tuples from the boto3 event stream.""" - for event in event_stream: - if "contentBlockStart" in event: - yield "contentBlockStart", event["contentBlockStart"] - elif "contentBlockDelta" in event: - yield "contentBlockDelta", event["contentBlockDelta"] - elif "contentBlockStop" in event: - yield "contentBlockStop", event["contentBlockStop"] - elif "messageStop" in event: - yield "messageStop", event["messageStop"] - elif "internalServerException" in event: - yield "internalServerException", event["internalServerException"] - elif "runtimeClientError" in event: - yield "runtimeClientError", event["runtimeClientError"] - - -def print_stream(event_stream): + + +def parse_events(http_response): + """Yield (event_type, payload) tuples from the harness binary event stream. + + The response arrives as raw bytes in AWS binary event stream format. + EventStreamBuffer reassembles complete events from the 4KB chunks, + and we decode each event's JSON payload before yielding it. + """ + event_buffer = EventStreamBuffer() + for chunk in http_response.stream(4096): + event_buffer.add_data(chunk) + for event in event_buffer: + if event.headers.get(":message-type") == "exception": + payload = json.loads(event.payload.decode("utf-8")) + print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) + sys.exit(1) + event_type = event.headers.get(":event-type", "") + if event.payload: + yield event_type, json.loads(event.payload.decode("utf-8")) + + +def print_stream(http_response): """Display harness events with GitHub Actions log groups. The harness streams events as the agent works: @@ -94,7 +112,7 @@ def flush_text(): print(f"{DIM}{line}{RESET}", flush=True) text_buffer = "" - for event_type, payload in parse_events(event_stream): + for event_type, payload in parse_events(http_response): if event_type == "contentBlockStart": start = payload.get("start", {}) @@ -153,11 +171,6 @@ def flush_text(): print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) sys.exit(1) - elif event_type == "runtimeClientError": - close_group() - print(f"\n{RED}ERROR: {payload.get('message', payload)}{RESET}", file=sys.stderr) - sys.exit(1) - close_group() total = time.time() - start_time print(f"\n{GREEN}Review complete.{RESET} {DIM}({iteration} tool calls, {int(total)}s total){RESET}") @@ -187,10 +200,18 @@ def flush_text(): SYSTEM_PROMPT = read_prompt("system.md") REVIEW_PROMPT = read_prompt("review.md").format(pr_url=PR_URL) -messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] +request_body = json.dumps({ + "runtimeSessionId": SESSION_ID, + "systemPrompt": [{"text": SYSTEM_PROMPT}], + "messages": [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}], + "model": {"bedrockModelConfig": {"modelId": MODEL_ID}}, +}) + +http_response = invoke_harness(HARNESS_ARN, request_body, REGION) -event_stream = invoke_harness_streaming( - HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION -) +if http_response.status != 200: + error = http_response.read().decode("utf-8") + print(f"{RED}ERROR: HTTP {http_response.status}: {error}{RESET}", file=sys.stderr) + sys.exit(1) -print_stream(event_stream) +print_stream(http_response) diff --git a/.github/workflows/pr-ai-review.yml b/.github/workflows/pr-ai-review.yml index 26878d7a1..8ababddcc 100644 --- a/.github/workflows/pr-ai-review.yml +++ b/.github/workflows/pr-ai-review.yml @@ -139,7 +139,7 @@ jobs: env: PR_URL: ${{ steps.pr-url.outputs.url }} HARNESS_ARN: ${{ secrets.HARNESS_ARN }} - run: python .github/harness/harness_review.py + run: python .github/scripts/python/harness_review.py - name: Remove agentcore-harness-reviewing label if: always() diff --git a/.prettierignore b/.prettierignore index 3b1452b18..8eda17e39 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ CHANGELOG.md src/assets/**/*.md -.github/harness/prompts/ +.github/scripts/prompts/ From ad2ba9bcc610ac4dc1ee255e5b5d407e6003c94a Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 30 Apr 2026 11:03:33 -0400 Subject: [PATCH 2/2] refactor: move harness resources to .github/harness/ and use boto3 invoke_harness - Move harness_review.py, prompts/ to .github/harness/ - Add Dockerfile for the harness container (dual-token setup) - Add README documenting the harness directory - Update pr-ai-review workflow to reference new path - Replace manual SigV4 signing + urllib3 with native boto3 invoke_harness - Update .prettierignore for new prompts location --- .github/harness/Dockerfile | 33 ++++++ .github/harness/README.md | 39 +++++++ .../python => harness}/harness_review.py | 107 +++++++----------- .../{scripts => harness}/prompts/review.md | 0 .../{scripts => harness}/prompts/system.md | 0 .github/workflows/pr-ai-review.yml | 2 +- .prettierignore | 2 +- 7 files changed, 117 insertions(+), 66 deletions(-) create mode 100644 .github/harness/Dockerfile create mode 100644 .github/harness/README.md rename .github/{scripts/python => harness}/harness_review.py (62%) rename .github/{scripts => harness}/prompts/review.md (100%) rename .github/{scripts => harness}/prompts/system.md (100%) diff --git a/.github/harness/Dockerfile b/.github/harness/Dockerfile new file mode 100644 index 000000000..3deec1a46 --- /dev/null +++ b/.github/harness/Dockerfile @@ -0,0 +1,33 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Install GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# Tokens are baked into the image at build time. This image must be treated as a +# secret and stored only in a registry with equivalent access controls. +ARG CLONE_TOKEN +ARG GITHUB_TOKEN + +# Configure git to use clone token for HTTPS clones +RUN git config --global url."https://${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" + +# Persist gh CLI auth so GITHUB_TOKEN doesn't need to be in the environment +RUN mkdir -p /root/.config/gh \ + && echo "github.com:" > /root/.config/gh/hosts.yml \ + && echo " oauth_token: ${GITHUB_TOKEN}" >> /root/.config/gh/hosts.yml \ + && echo " user: agentcore-cli-automation" >> /root/.config/gh/hosts.yml \ + && echo " git_protocol: https" >> /root/.config/gh/hosts.yml + +WORKDIR /opt/workspace diff --git a/.github/harness/README.md b/.github/harness/README.md new file mode 100644 index 000000000..d9ba15c61 --- /dev/null +++ b/.github/harness/README.md @@ -0,0 +1,39 @@ +# Harness Resources + +Container and scripts for AI-powered automation via +[AgentCore Harness](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html). + +## Structure + +``` +harness/ +├── Dockerfile # Container image for the harness runtime +├── harness_review.py # Invokes the harness to review PRs (SigV4 + event stream) +└── prompts/ + ├── system.md # System prompt (workspace context) + └── review.md # PR review task prompt +``` + +## Current: PR Reviewer + +Reviews pull requests on open/reopen via `.github/workflows/pr-ai-review.yml`. + +### Dual-token setup + +The Dockerfile takes two build args: + +- **`CLONE_TOKEN`** — baked into git config for cloning private repos +- **`GITHUB_TOKEN`** — baked into `gh` CLI auth for posting PR comments + +### Building the container + +```bash +finch build \ + --build-arg CLONE_TOKEN= \ + --build-arg GITHUB_TOKEN= \ + -t pr-reviewer .github/harness/ +``` + +## Future: Tester + +This directory will also house a harness-based test runner. diff --git a/.github/scripts/python/harness_review.py b/.github/harness/harness_review.py similarity index 62% rename from .github/scripts/python/harness_review.py rename to .github/harness/harness_review.py index fbfd0b0f9..455fb6a3d 100644 --- a/.github/scripts/python/harness_review.py +++ b/.github/harness/harness_review.py @@ -1,7 +1,7 @@ """Invoke Bedrock AgentCore Harness to review a GitHub PR. Reads PR_URL from the environment. Streams harness output to stdout. -Uses raw HTTP with SigV4 signing — no custom service model needed. +Uses the boto3 bedrock-agentcore client's invoke_harness API. """ import json @@ -11,11 +11,6 @@ import uuid import boto3 -from botocore.auth import SigV4Auth -from botocore.awsrequest import AWSRequest -from botocore.eventstream import EventStreamBuffer -from urllib.parse import quote -import urllib3 # ANSI color codes CYAN = "\033[36m" @@ -25,7 +20,7 @@ DIM = "\033[2m" RESET = "\033[0m" -SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..") +SCRIPTS_DIR = os.path.dirname(__file__) def read_prompt(filename): @@ -35,50 +30,37 @@ def read_prompt(filename): return f.read() -def invoke_harness(harness_arn, body, region): - """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response. - - InvokeHarness is not in standard boto3, so we call the REST API directly. - boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.) - and sign the request with SigV4. The response is an AWS binary event stream. - """ - session = boto3.Session(region_name=region) - credentials = session.get_credentials().get_frozen_credentials() - url = f"https://bedrock-agentcore.{region}.amazonaws.com/harnesses/invoke?harnessArn={quote(harness_arn, safe='')}" - request = AWSRequest(method="POST", url=url, data=body, headers={ - "Content-Type": "application/json", - "Accept": "application/vnd.amazon.eventstream", - }) - SigV4Auth(credentials, "bedrock-agentcore", region).add_auth(request) - return urllib3.PoolManager().urlopen( - "POST", url, body=body, - headers=dict(request.headers), - preload_content=False, - timeout=urllib3.Timeout(connect=10, read=600), +def invoke_harness_streaming(harness_arn, session_id, system_prompt, messages, model_id, region): + """Call invoke_harness via boto3 and return the event stream.""" + client = boto3.client("bedrock-agentcore", region_name=region) + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + systemPrompt=[{"text": system_prompt}], + messages=messages, + model={"bedrockModelConfig": {"modelId": model_id}}, ) - - -def parse_events(http_response): - """Yield (event_type, payload) tuples from the harness binary event stream. - - The response arrives as raw bytes in AWS binary event stream format. - EventStreamBuffer reassembles complete events from the 4KB chunks, - and we decode each event's JSON payload before yielding it. - """ - event_buffer = EventStreamBuffer() - for chunk in http_response.stream(4096): - event_buffer.add_data(chunk) - for event in event_buffer: - if event.headers.get(":message-type") == "exception": - payload = json.loads(event.payload.decode("utf-8")) - print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) - sys.exit(1) - event_type = event.headers.get(":event-type", "") - if event.payload: - yield event_type, json.loads(event.payload.decode("utf-8")) - - -def print_stream(http_response): + return response["stream"] + + +def parse_events(event_stream): + """Yield (event_type, payload) tuples from the boto3 event stream.""" + for event in event_stream: + if "contentBlockStart" in event: + yield "contentBlockStart", event["contentBlockStart"] + elif "contentBlockDelta" in event: + yield "contentBlockDelta", event["contentBlockDelta"] + elif "contentBlockStop" in event: + yield "contentBlockStop", event["contentBlockStop"] + elif "messageStop" in event: + yield "messageStop", event["messageStop"] + elif "internalServerException" in event: + yield "internalServerException", event["internalServerException"] + elif "runtimeClientError" in event: + yield "runtimeClientError", event["runtimeClientError"] + + +def print_stream(event_stream): """Display harness events with GitHub Actions log groups. The harness streams events as the agent works: @@ -112,7 +94,7 @@ def flush_text(): print(f"{DIM}{line}{RESET}", flush=True) text_buffer = "" - for event_type, payload in parse_events(http_response): + for event_type, payload in parse_events(event_stream): if event_type == "contentBlockStart": start = payload.get("start", {}) @@ -171,6 +153,11 @@ def flush_text(): print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) sys.exit(1) + elif event_type == "runtimeClientError": + close_group() + print(f"\n{RED}ERROR: {payload.get('message', payload)}{RESET}", file=sys.stderr) + sys.exit(1) + close_group() total = time.time() - start_time print(f"\n{GREEN}Review complete.{RESET} {DIM}({iteration} tool calls, {int(total)}s total){RESET}") @@ -200,18 +187,10 @@ def flush_text(): SYSTEM_PROMPT = read_prompt("system.md") REVIEW_PROMPT = read_prompt("review.md").format(pr_url=PR_URL) -request_body = json.dumps({ - "runtimeSessionId": SESSION_ID, - "systemPrompt": [{"text": SYSTEM_PROMPT}], - "messages": [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}], - "model": {"bedrockModelConfig": {"modelId": MODEL_ID}}, -}) - -http_response = invoke_harness(HARNESS_ARN, request_body, REGION) +messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] -if http_response.status != 200: - error = http_response.read().decode("utf-8") - print(f"{RED}ERROR: HTTP {http_response.status}: {error}{RESET}", file=sys.stderr) - sys.exit(1) +event_stream = invoke_harness_streaming( + HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION +) -print_stream(http_response) +print_stream(event_stream) diff --git a/.github/scripts/prompts/review.md b/.github/harness/prompts/review.md similarity index 100% rename from .github/scripts/prompts/review.md rename to .github/harness/prompts/review.md diff --git a/.github/scripts/prompts/system.md b/.github/harness/prompts/system.md similarity index 100% rename from .github/scripts/prompts/system.md rename to .github/harness/prompts/system.md diff --git a/.github/workflows/pr-ai-review.yml b/.github/workflows/pr-ai-review.yml index 8ababddcc..26878d7a1 100644 --- a/.github/workflows/pr-ai-review.yml +++ b/.github/workflows/pr-ai-review.yml @@ -139,7 +139,7 @@ jobs: env: PR_URL: ${{ steps.pr-url.outputs.url }} HARNESS_ARN: ${{ secrets.HARNESS_ARN }} - run: python .github/scripts/python/harness_review.py + run: python .github/harness/harness_review.py - name: Remove agentcore-harness-reviewing label if: always() diff --git a/.prettierignore b/.prettierignore index 8eda17e39..3b1452b18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ CHANGELOG.md src/assets/**/*.md -.github/scripts/prompts/ +.github/harness/prompts/