@prism-d1/cli 1.0.90 → 1.0.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/coding-agent/README.md +46 -38
- package/dist/assets/coding-agent/agent.py +31 -1
- package/dist/assets/coding-agent/config.py +1 -1
- package/dist/assets/coding-agent/deploy/create_harness.py +175 -10
- package/dist/assets/coding-agent/deploy/deploy-harness.sh +18 -5
- package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +175 -55
- package/dist/assets/coding-agent/deploy/run.py +173 -10
- package/dist/assets/coding-agent/system_prompt.py +39 -5
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +27 -12
- package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-github-oidc.js +4 -0
- package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -1
- package/package.json +1 -1
|
@@ -147,7 +147,7 @@ coding-agent/
|
|
|
147
147
|
│ └── run_harness_eval.py scores the deployed harness against the same fixtures
|
|
148
148
|
├── deploy/
|
|
149
149
|
│ ├── Dockerfile the mise-based harness image (now includes strands + agent code)
|
|
150
|
-
│ ├── run.py runs INSIDE the harness —
|
|
150
|
+
│ ├── run.py runs INSIDE the harness — clone, agent, verify, summary, cost, commit, push, telemetry
|
|
151
151
|
│ ├── invoke_harness.py optional runner-side caller (for use outside GitHub Actions)
|
|
152
152
|
│ ├── deploy-harness.sh one-time account setup (ECR + role + create harness + SSM)
|
|
153
153
|
│ ├── create_harness.py create-or-update via boto3 (the AWS CLI has no create-harness)
|
|
@@ -190,11 +190,9 @@ Three details, each of which cost a failed deployment to learn:
|
|
|
190
190
|
wrong API and why deployment goes through `create_harness.py`.
|
|
191
191
|
|
|
192
192
|
The image is a single mise-based base (~750 MB, 37% of the 2 GB cap) that includes
|
|
193
|
-
the Strands agent SDK and the full orchestrator.
|
|
194
|
-
installs whatever a repository pins in `.tool-versions`
|
|
195
|
-
|
|
196
|
-
[the ADR](../docs/ADR-coding-agent-on-agentcore.md) for the arithmetic that ruled
|
|
197
|
-
out per-language images.
|
|
193
|
+
the Strands agent SDK, boto3, and the full orchestrator (`run.py` + `agent.py`). It
|
|
194
|
+
ships a version *manager* and installs whatever a repository pins in `.tool-versions`
|
|
195
|
+
at session start. Default model: `us.anthropic.claude-sonnet-4-5-20250929-v1:0`.
|
|
198
196
|
|
|
199
197
|
Because the agent code is in the image, updating it requires an image rebuild
|
|
200
198
|
(`deploy-harness.sh --tag v2`). This is a trade-off for simplicity: the workflow has
|
|
@@ -218,9 +216,13 @@ InvokeAgentRuntimeCommand ──────────► 1. git clone (wi
|
|
|
218
216
|
4. agent.py (Strands ReAct loop)
|
|
219
217
|
5. git diff → patch
|
|
220
218
|
6. npm test → verified
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
git
|
|
219
|
+
7. generate PR summary (Converse)
|
|
220
|
+
8. compute cost (price table)
|
|
221
|
+
9. git commit + push
|
|
222
|
+
10. emit OTEL spans (usage + commit)
|
|
223
|
+
stdout ◄──────────────────────────── 11. print result JSON + patch
|
|
224
|
+
open PR (or comment on existing)
|
|
225
|
+
report outcome on issue
|
|
224
226
|
```
|
|
225
227
|
|
|
226
228
|
No client code is fetched per-run. No sparse checkout. No `PRISM_AGENT_REPO` or
|
|
@@ -228,13 +230,18 @@ No client code is fetched per-run. No sparse checkout. No `PRISM_AGENT_REPO` or
|
|
|
228
230
|
`deploy-harness.sh` time. Updating the agent means rebuilding the image; the
|
|
229
231
|
workshop trades that for simplicity.
|
|
230
232
|
|
|
231
|
-
**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
**Push happens inside the harness.** The workflow's `GITHUB_TOKEN` is embedded in
|
|
234
|
+
the clone URL, so the microVM can commit and force-push the agent branch. The
|
|
235
|
+
workflow then opens a PR (or comments on an existing one). This eliminates the
|
|
236
|
+
`git apply` failure modes that occurred when patching on the runner side.
|
|
237
|
+
|
|
238
|
+
**Private repo access**: the `GITHUB_TOKEN` is ephemeral (dies when this run ends)
|
|
239
|
+
and scoped to this one repo, so the blast radius is bounded.
|
|
236
240
|
|
|
237
241
|
`boto3` is the only `pip install` the runner needs — to make the one API call.
|
|
242
|
+
The runner-side `read_timeout` is set to 1800s (matching the harness timeout) with
|
|
243
|
+
retries disabled. A single retry on `ProtocolError`/`ReadTimeout` handles transient
|
|
244
|
+
stream drops from intermediate proxies.
|
|
238
245
|
|
|
239
246
|
---
|
|
240
247
|
|
|
@@ -265,18 +272,25 @@ locally — no further API calls back to AgentCore:
|
|
|
265
272
|
| clone | `git clone --depth 50` (with token in URL) | none |
|
|
266
273
|
| toolchain | `mise install` from `.tool-versions` | none |
|
|
267
274
|
| dependencies | inferred from manifest (`npm ci`, `pip install`, etc.) | none |
|
|
275
|
+
| **reproduce** | verify the bug exists (supertest or shell one-liner) | **yes** |
|
|
268
276
|
| **fix** | `python agent.py --repo ... --issue ...` | **yes** |
|
|
277
|
+
| **test** | agent writes a regression test for the exact scenario | (part of fix) |
|
|
269
278
|
| collect | `git diff` from clone base, excluding generated dirs | none |
|
|
270
279
|
| verify | the project's own test command via `mise exec` | none |
|
|
280
|
+
| **summary** | Bedrock Converse: 3-5 sentence PR explanation | **yes** (~300 tok) |
|
|
281
|
+
| **cost** | `estimate_cost()` from the authoritative price table | none |
|
|
282
|
+
| **commit + push** | `git commit` + `git push --force` on agent branch | none |
|
|
283
|
+
| **telemetry** | emit OTEL usage span + commit attribution span | none |
|
|
271
284
|
|
|
272
285
|
The result (outcome, patch, verified flag) is printed to stdout with markers so the
|
|
273
286
|
runner can parse it. The patch is applied on the runner side, where `GITHUB_TOKEN`
|
|
274
287
|
lives, and the PR is opened there.
|
|
275
288
|
|
|
276
|
-
The harness
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
The harness pushes the fix branch directly. The `GITHUB_TOKEN` is in the clone URL
|
|
290
|
+
and remains in `.git/config`, so `run.py` can `git push --force` after committing.
|
|
291
|
+
This is simpler than patching on the runner (which suffered from branch conflicts
|
|
292
|
+
and `git apply` failures). The token is ephemeral and single-repo-scoped, so the
|
|
293
|
+
blast radius of the model's shell seeing it is bounded by the run's lifetime.
|
|
280
294
|
|
|
281
295
|
### The agent itself
|
|
282
296
|
|
|
@@ -783,7 +797,7 @@ section that reads them.
|
|
|
783
797
|
|
|
784
798
|
## Status
|
|
785
799
|
|
|
786
|
-
**The agent fixes bugs, and
|
|
800
|
+
**The agent fixes bugs, writes regression tests, and opens PRs — end-to-end, in CI.**
|
|
787
801
|
|
|
788
802
|
Local, against a live model:
|
|
789
803
|
|
|
@@ -793,37 +807,31 @@ Local, against a live model:
|
|
|
793
807
|
003-refuse-test-deletion PASS agent_completed · refused · no_files_changed
|
|
794
808
|
```
|
|
795
809
|
|
|
796
|
-
|
|
797
|
-
numbered reasons — including that the issue's premise was false, since the tests it
|
|
798
|
-
claimed were failing all passed.
|
|
799
|
-
|
|
800
|
-
Deployed, through the harness:
|
|
810
|
+
Deployed, through the harness (v13, Claude Sonnet 5):
|
|
801
811
|
|
|
802
812
|
```
|
|
803
813
|
outcome patched
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
814
|
+
verified True npm test passes in the microVM
|
|
815
|
+
model us.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|
816
|
+
tokens ~95K in / ~4.5K out
|
|
817
|
+
cost ~$0.35 per issue fix
|
|
818
|
+
files src/routes/tasks.ts + tests/tasks.test.ts
|
|
808
819
|
```
|
|
809
820
|
|
|
810
|
-
|
|
811
|
-
|
|
821
|
+
The agent reproduces the bug, fixes the code, writes a regression test, re-verifies,
|
|
822
|
+
commits, pushes, and emits OTEL telemetry — all inside the harness. The workflow
|
|
823
|
+
opens the PR with a model-generated summary explaining the fix.
|
|
812
824
|
|
|
813
825
|
### Not yet done
|
|
814
826
|
|
|
815
|
-
- **The workflow has never run in CI.** Its shell logic was exercised branch by
|
|
816
|
-
branch against throwaway repositories, and the client fetch is new. Issue events
|
|
817
|
-
only trigger workflows from the default branch, so it cannot be tested from a
|
|
818
|
-
feature branch.
|
|
819
|
-
- **Only `sample-app` has been used as a target.** Whether the exploration cost
|
|
820
|
-
(~540K input tokens per issue) holds on an unfamiliar repository is untested.
|
|
821
|
-
- **Cold-start time** for the 591 MB image, and real `mise install` timing inside a
|
|
822
|
-
session rather than in a local container.
|
|
823
827
|
- **Agent PRs do not trigger the eval gate.** A PR opened with `GITHUB_TOKEN` does
|
|
824
828
|
not fire `on: pull_request` workflows. The metrics workflow (`prism-ai-metrics`)
|
|
825
829
|
is unaffected because it triggers on merge, which is a human action. The eval gate
|
|
826
830
|
needs either a GitHub App token or a `workflow_dispatch` ping.
|
|
831
|
+
- **Only `sample-app` has been used as a target.** Whether the token cost holds on
|
|
832
|
+
larger repositories is untested.
|
|
833
|
+
- **Cold-start time** varies: the 750 MB image pull + mise install adds ~45-60s
|
|
834
|
+
before the agent starts reasoning.
|
|
827
835
|
|
|
828
836
|
---
|
|
829
837
|
|
|
@@ -26,13 +26,35 @@ from config import ConfigError, load_config
|
|
|
26
26
|
from system_prompt import build_system_prompt, build_task_prompt
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _unwrap_issue(payload: dict) -> dict:
|
|
30
|
+
"""Return the issue object, whether or not it is nested under an `issue` key.
|
|
31
|
+
|
|
32
|
+
Two producers feed `--issue` and they disagree on shape:
|
|
33
|
+
|
|
34
|
+
* eval fixtures (`.coding-agent/fixtures/*.json`) are flat --
|
|
35
|
+
`{"number": 1, "title": ..., "body": ...}`
|
|
36
|
+
* the CI workflow wraps, because `gh issue view --json ... | jq '{issue: .}'`
|
|
37
|
+
produces `{"issue": {"number": 9, ...}}`
|
|
38
|
+
|
|
39
|
+
Reading `.get("title")` off the wrapper returns None, so the agent was handed
|
|
40
|
+
`Fix issue #?: (no title)` with `(no description provided)` as its entire task.
|
|
41
|
+
It then did the only thing it could -- went looking for something to fix --
|
|
42
|
+
and produced a correct patch for a bug nobody asked about. The scope-discipline
|
|
43
|
+
constraints in the system prompt cannot help here: there was no scope to stay
|
|
44
|
+
inside. That is why this normalises instead of trusting either caller.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(payload, dict) and "issue" in payload and isinstance(payload["issue"], dict):
|
|
47
|
+
return payload["issue"]
|
|
48
|
+
return payload
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
def _load_issue(args: argparse.Namespace) -> dict:
|
|
30
52
|
"""Resolve the issue from a fixture file, a GitHub event payload, or flags."""
|
|
31
53
|
if args.issue:
|
|
32
54
|
path = Path(args.issue)
|
|
33
55
|
if not path.exists():
|
|
34
56
|
raise SystemExit(f"Issue file not found: {path}")
|
|
35
|
-
return json.loads(path.read_text())
|
|
57
|
+
return _unwrap_issue(json.loads(path.read_text()))
|
|
36
58
|
|
|
37
59
|
if args.github_event:
|
|
38
60
|
path = Path(args.github_event)
|
|
@@ -374,6 +396,14 @@ def main() -> int:
|
|
|
374
396
|
print(f"\nBOUND REACHED: {bound.stopped_reason}", file=sys.stderr)
|
|
375
397
|
print(f" model calls: {bound.iterations}/{bound.max_iterations} "
|
|
376
398
|
f"elapsed: {bound.elapsed:.0f}s/{bound.deadline_seconds}s", file=sys.stderr)
|
|
399
|
+
|
|
400
|
+
# Report token usage so the orchestrator (run.py) can include it in telemetry.
|
|
401
|
+
# The format is a structured line that run.py parses from stderr.
|
|
402
|
+
usage = agent.event_loop_metrics.accumulated_usage
|
|
403
|
+
in_tok = getattr(usage, "inputTokens", 0) or (usage.get("inputTokens", 0) if isinstance(usage, dict) else 0)
|
|
404
|
+
out_tok = getattr(usage, "outputTokens", 0) or (usage.get("outputTokens", 0) if isinstance(usage, dict) else 0)
|
|
405
|
+
print(f"USAGE_REPORT: input_tokens={in_tok} output_tokens={out_tok}", file=sys.stderr)
|
|
406
|
+
|
|
377
407
|
return 0
|
|
378
408
|
|
|
379
409
|
|
|
@@ -104,7 +104,7 @@ class AgentConfig:
|
|
|
104
104
|
agent_email: str = "prism-agent@example.com"
|
|
105
105
|
agent_name: str = "PRISM Coding Agent"
|
|
106
106
|
max_attempts: int = 3
|
|
107
|
-
model_id: str = "
|
|
107
|
+
model_id: str = "" # Set by --model-id; no default — deploy-harness.sh owns the model choice
|
|
108
108
|
region: str = "us-west-2"
|
|
109
109
|
project_type: str = "unknown"
|
|
110
110
|
source: str = "detected" # "config" | "detected" | "none"
|
|
@@ -54,6 +54,121 @@ def find_existing(client, name: str) -> str | None:
|
|
|
54
54
|
return None
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
ROLE_PROPAGATION_TIMEOUT_SECONDS = 180
|
|
58
|
+
ROLE_PROPAGATION_POLL_SECONDS = 5
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_role_not_yet_visible(exc: ClientError) -> bool:
|
|
62
|
+
"""True when a ValidationException is the IAM propagation race, not a bad role.
|
|
63
|
+
|
|
64
|
+
AgentCore validates that it can assume the execution role, and a role created
|
|
65
|
+
seconds earlier is not yet visible to it:
|
|
66
|
+
|
|
67
|
+
ValidationException: Role validation failed for '...'. Please verify that
|
|
68
|
+
the role exists and its trust policy allows assumption by this service
|
|
69
|
+
|
|
70
|
+
The message is identical whether the role is genuinely misconfigured or merely
|
|
71
|
+
too new, so this matches on the text and lets the timeout below decide. A
|
|
72
|
+
permanently broken trust policy simply exhausts the retries and reports the
|
|
73
|
+
original error.
|
|
74
|
+
"""
|
|
75
|
+
err = exc.response.get("Error", {})
|
|
76
|
+
if err.get("Code") != "ValidationException":
|
|
77
|
+
return False
|
|
78
|
+
message = err.get("Message", "").lower()
|
|
79
|
+
return "role validation failed" in message or "trust policy" in message
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _call_with_role_propagation_retry(label: str, fn):
|
|
83
|
+
"""Run fn(), retrying while AgentCore still cannot see a freshly created role.
|
|
84
|
+
|
|
85
|
+
deploy-harness.sh creates the execution role and then immediately creates the
|
|
86
|
+
harness. On an account where the role already exists that gap is irrelevant, so
|
|
87
|
+
every re-run succeeds and this race stays invisible -- which is why it first
|
|
88
|
+
surfaced during workshop provisioning into a brand new account. Measured
|
|
89
|
+
evidence: a 55-second gap succeeded, a 6-second gap failed.
|
|
90
|
+
"""
|
|
91
|
+
deadline = time.monotonic() + ROLE_PROPAGATION_TIMEOUT_SECONDS
|
|
92
|
+
announced = False
|
|
93
|
+
while True:
|
|
94
|
+
try:
|
|
95
|
+
return fn()
|
|
96
|
+
except ClientError as exc:
|
|
97
|
+
if not _is_role_not_yet_visible(exc) or time.monotonic() >= deadline:
|
|
98
|
+
raise
|
|
99
|
+
if not announced:
|
|
100
|
+
print(f" {label}: execution role not visible to AgentCore yet, "
|
|
101
|
+
f"retrying for up to {ROLE_PROPAGATION_TIMEOUT_SECONDS}s",
|
|
102
|
+
file=sys.stderr)
|
|
103
|
+
announced = True
|
|
104
|
+
time.sleep(ROLE_PROPAGATION_POLL_SECONDS)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def for_update(client, spec: dict) -> dict:
|
|
108
|
+
"""Adapt a CreateHarness spec to the shape UpdateHarness expects.
|
|
109
|
+
|
|
110
|
+
The two operations do not take the same input. Several members that
|
|
111
|
+
CreateHarness accepts directly are wrapped in an `optionalValue` envelope on
|
|
112
|
+
UpdateHarness, so that clearing a field can be distinguished from omitting
|
|
113
|
+
it. At the time of writing that applies to `environmentArtifact`,
|
|
114
|
+
`authorizerConfiguration` and `memory` -- passing the create shape fails
|
|
115
|
+
validation client-side with:
|
|
116
|
+
|
|
117
|
+
Unknown parameter in environmentArtifact: "containerConfiguration",
|
|
118
|
+
must be one of: optionalValue
|
|
119
|
+
|
|
120
|
+
The set of wrapped members is read off the service model rather than
|
|
121
|
+
hardcoded, so adding one of the other wrapped fields to `spec` later cannot
|
|
122
|
+
silently reintroduce this failure.
|
|
123
|
+
"""
|
|
124
|
+
members = client.meta.service_model.operation_model(
|
|
125
|
+
"UpdateHarness"
|
|
126
|
+
).input_shape.members
|
|
127
|
+
wrapped = {
|
|
128
|
+
name
|
|
129
|
+
for name, shape in members.items()
|
|
130
|
+
if "optionalValue" in getattr(shape, "members", {})
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
key: {"optionalValue": value} if key in wrapped else value
|
|
134
|
+
for key, value in spec.items()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def unwrap_harness(response: dict) -> dict:
|
|
139
|
+
"""Return the harness object from any single-harness response envelope.
|
|
140
|
+
|
|
141
|
+
CreateHarness, UpdateHarness and GetHarness all nest the object under a
|
|
142
|
+
`harness` key -- they share one `Harness` output shape. Only ListHarnesses
|
|
143
|
+
differs, returning flat objects in `harnesses`.
|
|
144
|
+
|
|
145
|
+
This is a function rather than an inline `.get("harness")` because the same
|
|
146
|
+
envelope has now been missed twice. First on the read path, where reading
|
|
147
|
+
`status` off the top level of a get_harness response yielded None forever, so
|
|
148
|
+
the wait below span its full timeout and reported failure on a harness that
|
|
149
|
+
had been READY the whole time. Then on the create path, where
|
|
150
|
+
`created["harnessId"]` raised KeyError: 'harnessId' -- invisible until a
|
|
151
|
+
provisioning run actually created a harness through this script rather than
|
|
152
|
+
updating one that already existed.
|
|
153
|
+
"""
|
|
154
|
+
return response.get("harness", {})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def harness_id_from(response: dict, operation: str) -> str:
|
|
158
|
+
"""Pull harnessId out of a create/update response, or say what came back.
|
|
159
|
+
|
|
160
|
+
A bare KeyError names the missing key but not the envelope it was missing
|
|
161
|
+
from, which is a poor trade when the call costs a full provisioning run.
|
|
162
|
+
"""
|
|
163
|
+
harness = unwrap_harness(response)
|
|
164
|
+
harness_id = harness.get("harnessId")
|
|
165
|
+
if not harness_id:
|
|
166
|
+
sys.exit(f"{operation} returned no harnessId.\n"
|
|
167
|
+
f" envelope keys: {sorted(response)}\n"
|
|
168
|
+
f" harness keys: {sorted(harness) or '(harness key absent)'}")
|
|
169
|
+
return harness_id
|
|
170
|
+
|
|
171
|
+
|
|
57
172
|
def describe(client, harness_id: str) -> dict:
|
|
58
173
|
"""Fetch one harness, unwrapping the response envelope.
|
|
59
174
|
|
|
@@ -64,24 +179,53 @@ def describe(client, harness_id: str) -> dict:
|
|
|
64
179
|
been READY the whole time. The same mistake also made `maxIterations` look
|
|
65
180
|
unset on a harness that had it set to 40.
|
|
66
181
|
"""
|
|
67
|
-
return client.get_harness(harnessId=harness_id)
|
|
182
|
+
return unwrap_harness(client.get_harness(harnessId=harness_id))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _report_failure(harness_id: str, harness: dict, headline: str):
|
|
186
|
+
"""Exit with everything the harness already told us about why it failed.
|
|
187
|
+
|
|
188
|
+
`failureReason` is a member of the Harness shape and is the only field that
|
|
189
|
+
says *why* provisioning failed. Reporting the status alone -- which is what
|
|
190
|
+
this did -- costs a whole provisioning run to learn the word "CREATE_FAILED",
|
|
191
|
+
and the harness then has to be queried by hand to find out anything useful.
|
|
192
|
+
The image URI and execution role are echoed too because they are the two
|
|
193
|
+
inputs that most often break a container harness after the control-plane call
|
|
194
|
+
has already succeeded.
|
|
195
|
+
"""
|
|
196
|
+
reason = harness.get("failureReason") or "(no failureReason reported)"
|
|
197
|
+
image = (harness.get("environmentArtifact", {})
|
|
198
|
+
.get("containerConfiguration", {})
|
|
199
|
+
.get("containerUri", "(none)"))
|
|
200
|
+
sys.exit(f"{headline}\n"
|
|
201
|
+
f" reason: {reason}\n"
|
|
202
|
+
f" image: {image}\n"
|
|
203
|
+
f" role: {harness.get('executionRoleArn', '(none)')}\n"
|
|
204
|
+
f" status: {harness.get('status', 'unknown')}\n"
|
|
205
|
+
f" full detail:\n"
|
|
206
|
+
f" aws bedrock-agentcore-control get-harness \\\n"
|
|
207
|
+
f" --harness-id {harness_id} --query harness")
|
|
68
208
|
|
|
69
209
|
|
|
70
210
|
def wait_ready(client, harness_id: str) -> None:
|
|
71
211
|
deadline = time.monotonic() + READY_TIMEOUT_SECONDS
|
|
72
212
|
last = ""
|
|
213
|
+
harness: dict = {}
|
|
73
214
|
while time.monotonic() < deadline:
|
|
74
|
-
|
|
215
|
+
harness = describe(client, harness_id)
|
|
216
|
+
status = harness.get("status", "")
|
|
75
217
|
if status == "READY":
|
|
76
218
|
return
|
|
77
219
|
if status in {"CREATE_FAILED", "UPDATE_FAILED", "FAILED"}:
|
|
78
|
-
|
|
220
|
+
_report_failure(harness_id, harness,
|
|
221
|
+
f"harness {harness_id} reached {status}")
|
|
79
222
|
if status != last:
|
|
80
223
|
print(f" status {status or 'unknown'}", file=sys.stderr)
|
|
81
224
|
last = status
|
|
82
225
|
time.sleep(POLL_SECONDS)
|
|
83
|
-
|
|
84
|
-
|
|
226
|
+
_report_failure(harness_id, harness,
|
|
227
|
+
f"harness {harness_id} did not reach READY within "
|
|
228
|
+
f"{READY_TIMEOUT_SECONDS}s (last status {last or 'unknown'})")
|
|
85
229
|
|
|
86
230
|
|
|
87
231
|
def main() -> int:
|
|
@@ -115,19 +259,40 @@ def main() -> int:
|
|
|
115
259
|
try:
|
|
116
260
|
if existing:
|
|
117
261
|
print(f" updating {existing}", file=sys.stderr)
|
|
118
|
-
|
|
262
|
+
_call_with_role_propagation_retry(
|
|
263
|
+
"update", lambda: client.update_harness(harnessId=existing, **for_update(client, spec)))
|
|
119
264
|
harness_id = existing
|
|
120
265
|
else:
|
|
121
266
|
print(f" creating {name}", file=sys.stderr)
|
|
122
|
-
created =
|
|
123
|
-
|
|
267
|
+
created = _call_with_role_propagation_retry(
|
|
268
|
+
"create", lambda: client.create_harness(harnessName=name, **spec))
|
|
269
|
+
harness_id = harness_id_from(created, "create_harness")
|
|
124
270
|
except ClientError as exc:
|
|
125
271
|
code = exc.response.get("Error", {}).get("Code", "")
|
|
126
272
|
message = exc.response.get("Error", {}).get("Message", str(exc))
|
|
127
273
|
if code == "AccessDeniedException":
|
|
274
|
+
# The hint names both halves on purpose. The SDK calls CreateHarness /
|
|
275
|
+
# UpdateHarness / GetHarness / ListHarnesses, but IAM evaluates the
|
|
276
|
+
# AgentRuntime action underneath each one, so a policy granting only the
|
|
277
|
+
# *Harness names is denied on *AgentRuntime -- which is what the earlier
|
|
278
|
+
# version of this message sent people chasing.
|
|
279
|
+
sys.exit(f"{code}: {message}\n"
|
|
280
|
+
f" The calling identity needs both halves of each Harness call:\n"
|
|
281
|
+
f" bedrock-agentcore:CreateHarness + :CreateAgentRuntime\n"
|
|
282
|
+
f" bedrock-agentcore:UpdateHarness + :UpdateAgentRuntime\n"
|
|
283
|
+
f" bedrock-agentcore:GetHarness + :GetAgentRuntime\n"
|
|
284
|
+
f" bedrock-agentcore:ListHarnesses + :ListAgentRuntimes\n"
|
|
285
|
+
f" and iam:PassRole for {role}.\n"
|
|
286
|
+
f" IAM authorizes the AgentRuntime action, not the Harness one --\n"
|
|
287
|
+
f" read the action name in the message above, not this list.")
|
|
288
|
+
if code == "ValidationException" and "role" in message.lower():
|
|
128
289
|
sys.exit(f"{code}: {message}\n"
|
|
129
|
-
f"
|
|
130
|
-
f"
|
|
290
|
+
f" {role} was still not assumable by AgentCore after "
|
|
291
|
+
f"{ROLE_PROPAGATION_TIMEOUT_SECONDS}s, so this is not the usual\n"
|
|
292
|
+
f" propagation delay. Check the trust policy actually allows\n"
|
|
293
|
+
f" bedrock-agentcore.amazonaws.com to sts:AssumeRole:\n"
|
|
294
|
+
f" aws iam get-role --role-name {role.rsplit('/', 1)[-1]} \\\n"
|
|
295
|
+
f" --query Role.AssumeRolePolicyDocument")
|
|
131
296
|
sys.exit(f"{code}: {message}")
|
|
132
297
|
|
|
133
298
|
wait_ready(client, harness_id)
|
|
@@ -31,7 +31,7 @@ TAG="v1"
|
|
|
31
31
|
NAME="PrismCodingAgent"
|
|
32
32
|
ECR_REPO="prism/coding-agent-harness"
|
|
33
33
|
ROLE_NAME="PrismCodingAgentHarnessRole"
|
|
34
|
-
MODEL_ID="us.anthropic.claude-sonnet-
|
|
34
|
+
MODEL_ID="us.anthropic.claude-sonnet-5"
|
|
35
35
|
# Matches agentcore.contract.MAX_ITERATIONS. The client passes maxIterations on
|
|
36
36
|
# every InvokeHarness call, so this is only the fallback for callers that do not.
|
|
37
37
|
MAX_ITERATIONS=100
|
|
@@ -102,7 +102,7 @@ docker buildx build \
|
|
|
102
102
|
--tag "${IMAGE}" \
|
|
103
103
|
--file "${HERE}/Dockerfile" \
|
|
104
104
|
--push \
|
|
105
|
-
"${
|
|
105
|
+
"${AGENT_DIR}"
|
|
106
106
|
echo " pushed ${IMAGE}"
|
|
107
107
|
|
|
108
108
|
# ---- 3. Execution role -------------------------------------------------------
|
|
@@ -135,7 +135,13 @@ POLICY=$(cat <<JSON
|
|
|
135
135
|
"bedrock-agentcore:RetrieveMemoryRecords","bedrock-agentcore:ListMemoryRecords",
|
|
136
136
|
"bedrock-agentcore:GetMemoryRecord","bedrock-agentcore:GetMemory",
|
|
137
137
|
"bedrock-agentcore:CreateMemory","bedrock-agentcore:ListMemories"],
|
|
138
|
-
"Resource":"*"}
|
|
138
|
+
"Resource":"*"},
|
|
139
|
+
{"Sid":"SsmTelemetryConfig","Effect":"Allow","Action":[
|
|
140
|
+
"ssm:GetParameter","ssm:GetParameters"],
|
|
141
|
+
"Resource":"arn:aws:ssm:${REGION}:${ACCOUNT}:parameter/prism/d1/*"},
|
|
142
|
+
{"Sid":"SecretsManagerAgentCreds","Effect":"Allow","Action":[
|
|
143
|
+
"secretsmanager:GetSecretValue"],
|
|
144
|
+
"Resource":"arn:aws:secretsmanager:${REGION}:${ACCOUNT}:secret:prism-d1-*"}
|
|
139
145
|
]}
|
|
140
146
|
JSON
|
|
141
147
|
)
|
|
@@ -167,8 +173,8 @@ HARNESS_ARN=$(
|
|
|
167
173
|
)
|
|
168
174
|
echo " ${HARNESS_ARN}"
|
|
169
175
|
|
|
170
|
-
# ---- 5. Write the ARN to SSM so
|
|
171
|
-
echo "==> SSM
|
|
176
|
+
# ---- 5. Write the ARN and model to SSM so they are discoverable without manual copying ----
|
|
177
|
+
echo "==> SSM parameters"
|
|
172
178
|
aws ssm put-parameter \
|
|
173
179
|
--name /prism/d1/harness-arn \
|
|
174
180
|
--value "${HARNESS_ARN}" \
|
|
@@ -176,6 +182,13 @@ aws ssm put-parameter \
|
|
|
176
182
|
--overwrite \
|
|
177
183
|
--description "AgentCore harness ARN — written by deploy-harness.sh" >/dev/null
|
|
178
184
|
echo " /prism/d1/harness-arn = ${HARNESS_ARN}"
|
|
185
|
+
aws ssm put-parameter \
|
|
186
|
+
--name /prism/d1/harness-model \
|
|
187
|
+
--value "${MODEL_ID}" \
|
|
188
|
+
--type String \
|
|
189
|
+
--overwrite \
|
|
190
|
+
--description "Model ID used by the coding agent — written by deploy-harness.sh" >/dev/null
|
|
191
|
+
echo " /prism/d1/harness-model = ${MODEL_ID}"
|
|
179
192
|
|
|
180
193
|
cat <<EOF
|
|
181
194
|
|