@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.
@@ -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 — the full cycle in one script
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. It ships a version *manager* and
194
- installs whatever a repository pins in `.tool-versions` at session start, which is
195
- why `install-coding-agent` writes that file. See
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
- stdout ◄──────────────────────────── 7. print result JSON + patch
222
- apply patch
223
- git push + gh pr create
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
- **Private repo access**: the workflow's `GITHUB_TOKEN` is passed in the clone URL.
232
- It's ephemeral (dies when this run ends) and scoped to this one repo, so the blast
233
- radius is bounded. The model's shell tool *can* read it during the run — which is
234
- acceptable for the same reason it's acceptable that the model can read any file in
235
- the checkout: the boundary is the run's lifetime, not the tool's access.
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 never pushes. The token is in the URL for cloning, but push happens on
277
- the runner with `git push`. This keeps write access confined to the workflow step
278
- that has the scoped token, rather than inside the microVM where the model's shell
279
- could use it.
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 this has been observed rather than inferred.**
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
- Fixture 003 is the one worth reading the transcript for. It declined and gave four
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
- stop_reason end_turn finished deliberately, not cut off
805
- verified True the project's own `npm test`, run in the microVM
806
- added_files [] no scratch left behind
807
- patch 1,317 bytes, one file
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
- An earlier harness run also fixed the same defect in `src/mcp/tools.ts`, which the
811
- issue never mentioned.
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 = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
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).get("harness", {})
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
- status = describe(client, harness_id).get("status", "")
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
- sys.exit(f"harness {harness_id} reached {status}")
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
- sys.exit(f"harness {harness_id} did not reach READY within "
84
- f"{READY_TIMEOUT_SECONDS}s (last status {last or 'unknown'})")
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
- client.update_harness(harnessId=existing, **spec)
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 = client.create_harness(harnessName=name, **spec)
123
- harness_id = created["harnessId"]
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" The calling identity needs bedrock-agentcore:CreateHarness /\n"
130
- f" UpdateHarness / ListHarnesses, and iam:PassRole for {role}.")
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-4-5-20250929-v1:0"
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
- "${HERE}"
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 it is discoverable without manual copying ----
171
- echo "==> SSM parameter"
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