@deployfoundation/foundation-deploy 0.2.39 → 0.2.41

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/bin/app.js CHANGED
@@ -8,13 +8,13 @@ import {
8
8
  FoundationPages,
9
9
  FoundationPipeline,
10
10
  NewsletterStack
11
- } from "../chunk-6v0ewr5b.js";
11
+ } from "../chunk-sgh3agpa.js";
12
12
  import {
13
13
  adminsFor,
14
14
  instanceNames,
15
15
  loadInstanceFile,
16
16
  provisionsIntegration
17
- } from "../chunk-7rd8c1th.js";
17
+ } from "../chunk-g56mqyfx.js";
18
18
  import"../chunk-dz91avrb.js";
19
19
 
20
20
  // bin/app.ts
@@ -22,7 +22,7 @@ import {
22
22
  slackCommandPrefix,
23
23
  verifyManifest,
24
24
  versionedConfigKey
25
- } from "../chunk-7rd8c1th.js";
25
+ } from "../chunk-g56mqyfx.js";
26
26
  import {
27
27
  DEFAULT_RELEASE_BUCKET,
28
28
  expectedKeyArn
@@ -1043,6 +1043,21 @@ var SMOKE_EXPECT = ['"authenticated":true', '"config_source":"s3"'];
1043
1043
  function smokeSessionId(prefix) {
1044
1044
  return `${prefix}-${Math.floor(Date.now() / 1000)}`.padEnd(40, "0");
1045
1045
  }
1046
+ async function stopSmokeSession(runner, opts, log) {
1047
+ try {
1048
+ await runner.capture([
1049
+ "bedrock-agentcore",
1050
+ "stop-runtime-session",
1051
+ "--agent-runtime-arn",
1052
+ opts.arn,
1053
+ ...opts.qualifier === undefined ? [] : ["--qualifier", opts.qualifier],
1054
+ "--runtime-session-id",
1055
+ opts.sessionId
1056
+ ]);
1057
+ } catch {
1058
+ log(" (could not stop the smoke session; it ends at the idle timeout)");
1059
+ }
1060
+ }
1046
1061
  async function smokeInvoke(runner, opts) {
1047
1062
  const log = opts.log ?? ((line) => console.log(line));
1048
1063
  const payload = opts.payload ?? '{"_smoke_test":true}';
@@ -1070,6 +1085,7 @@ async function smokeInvoke(runner, opts) {
1070
1085
  } finally {
1071
1086
  await rm(dir, { recursive: true, force: true });
1072
1087
  }
1088
+ await stopSmokeSession(runner, opts, log);
1073
1089
  log(` ${out.replaceAll(`
1074
1090
  `, `
1075
1091
  `)}`);
@@ -1261,31 +1261,45 @@ var contextStorage = new AsyncLocalStorage;
1261
1261
  // ../core/src/config.ts
1262
1262
  import { createHash as createHash2 } from "node:crypto";
1263
1263
  import { parse as parseYaml2 } from "yaml";
1264
- import { z as z4 } from "zod";
1264
+ import { z as z5 } from "zod";
1265
1265
 
1266
1266
  // ../core/src/jobs.ts
1267
1267
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
1268
+ import { z as z4 } from "zod";
1269
+
1270
+ // ../core/src/thinking-level.ts
1268
1271
  import { z as z3 } from "zod";
1272
+ var ThinkingLevel = z3.enum(["none", "low", "medium", "high", "xhigh", "max"]);
1273
+
1274
+ // ../core/src/jobs.ts
1269
1275
  var JOB_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,40}$/;
1270
- var RawScheduleTrigger = z3.object({
1271
- schedule: z3.string().min(1)
1276
+ var RawScheduleTrigger = z4.object({
1277
+ schedule: z4.string().min(1)
1272
1278
  }).strict();
1273
- var RawWebhookTrigger = z3.object({
1274
- webhook: z3.object({}).strict()
1279
+ var RawWebhookTrigger = z4.object({
1280
+ webhook: z4.object({}).strict()
1275
1281
  }).strict();
1276
- var RawJob = z3.object({
1277
- id: z3.string().regex(JOB_ID_PATTERN),
1278
- name: z3.string().min(1).max(80),
1279
- channel: z3.string().regex(/^[CG][A-Z0-9]{8,}$/, "a job runs in a Slack channel id"),
1280
- on: z3.union([RawScheduleTrigger, RawWebhookTrigger]),
1281
- prompt: z3.string().min(1).max(4000)
1282
- }).strict();
1283
- var RawJobs = z3.array(RawJob).default([]).superRefine((jobs, ctx) => {
1282
+ var RawJob = z4.object({
1283
+ id: z4.string().regex(JOB_ID_PATTERN),
1284
+ name: z4.string().min(1).max(80),
1285
+ channel: z4.string().regex(/^[CG][A-Z0-9]{8,}$/, "a job runs in a Slack channel id"),
1286
+ on: z4.union([RawScheduleTrigger, RawWebhookTrigger]),
1287
+ prompt: z4.string().min(1).max(4000),
1288
+ thinkingLevel: ThinkingLevel.optional()
1289
+ }).strict().superRefine((job, ctx) => {
1290
+ if (job.thinkingLevel !== undefined && !("schedule" in job.on))
1291
+ ctx.addIssue({
1292
+ code: z4.ZodIssueCode.custom,
1293
+ path: ["thinkingLevel"],
1294
+ message: "thinkingLevel applies to scheduled jobs only"
1295
+ });
1296
+ });
1297
+ var RawJobs = z4.array(RawJob).default([]).superRefine((jobs, ctx) => {
1284
1298
  const seen = new Set;
1285
1299
  for (const [index, job] of jobs.entries()) {
1286
1300
  if (seen.has(job.id)) {
1287
1301
  ctx.addIssue({
1288
- code: z3.ZodIssueCode.custom,
1302
+ code: z4.ZodIssueCode.custom,
1289
1303
  path: [index, "id"],
1290
1304
  message: `duplicate job id: ${job.id}`
1291
1305
  });
@@ -1306,9 +1320,13 @@ function scheduledJobFingerprint(input) {
1306
1320
  return createHash("sha256").update(JSON.stringify([input.routineId, input.name, input.channel, input.prompt])).digest("hex");
1307
1321
  }
1308
1322
  function scheduledJobBindings(jobs) {
1309
- return jobs.filter(isScheduledJob).map(({ id, name, channel, prompt }) => {
1323
+ return jobs.filter(isScheduledJob).map(({ id, name, channel, prompt, thinkingLevel }) => {
1310
1324
  const input = { routineId: `job-${id}`, name, channel, prompt };
1311
- return { routineId: input.routineId, fingerprint: scheduledJobFingerprint(input) };
1325
+ return {
1326
+ routineId: input.routineId,
1327
+ fingerprint: scheduledJobFingerprint(input),
1328
+ ...thinkingLevel === undefined ? {} : { thinkingLevel }
1329
+ };
1312
1330
  });
1313
1331
  }
1314
1332
  function jobWebhookKey(instanceSecret, jobId) {
@@ -1374,11 +1392,10 @@ function capabilityScheduledJobs(config) {
1374
1392
 
1375
1393
  // ../core/src/config.ts
1376
1394
  var MODEL_RE = /^([a-z0-9-]+)\/(.+)$/;
1377
- var ThinkingLevel = z4.enum(["none", "low", "medium", "high", "xhigh", "max"]);
1378
- var RawImages = z4.object({
1379
- backend: z4.enum(["gemini"]).default("gemini"),
1380
- model: z4.string().min(1).default("gemini-2.5-flash-image"),
1381
- dailyCap: z4.number().int().min(0).default(40)
1395
+ var RawImages = z5.object({
1396
+ backend: z5.enum(["gemini"]).default("gemini"),
1397
+ model: z5.string().min(1).default("gemini-2.5-flash-image"),
1398
+ dailyCap: z5.number().int().min(0).default(40)
1382
1399
  }).strict().default({});
1383
1400
  var OMP_MODEL_ROLES = [
1384
1401
  "default",
@@ -1391,40 +1408,40 @@ var OMP_MODEL_ROLES = [
1391
1408
  "task",
1392
1409
  "advisor"
1393
1410
  ];
1394
- var RawModelRoles = z4.record(z4.enum(OMP_MODEL_ROLES), z4.string().regex(MODEL_RE)).default({});
1395
- var RawChannelModels = z4.record(z4.string().regex(/^[CDG][A-Z0-9]+$/, "channelModels keys must be Slack conversation ids"), z4.string().regex(MODEL_RE, "channel model must be provider/modelId")).default({});
1396
- var RawSubagents = z4.object({
1397
- enabled: z4.boolean().default(true),
1398
- maxConcurrency: z4.number().int().min(1).max(8).default(3),
1399
- maxRecursionDepth: z4.number().int().min(1).max(3).default(2),
1400
- effort: z4.boolean().default(true)
1411
+ var RawModelRoles = z5.record(z5.enum(OMP_MODEL_ROLES), z5.string().regex(MODEL_RE)).default({});
1412
+ var RawChannelModels = z5.record(z5.string().regex(/^[CDG][A-Z0-9]+$/, "channelModels keys must be Slack conversation ids"), z5.string().regex(MODEL_RE, "channel model must be provider/modelId")).default({});
1413
+ var RawSubagents = z5.object({
1414
+ enabled: z5.boolean().default(true),
1415
+ maxConcurrency: z5.number().int().min(1).max(8).default(3),
1416
+ maxRecursionDepth: z5.number().int().min(1).max(3).default(2),
1417
+ effort: z5.boolean().default(true)
1401
1418
  }).strict().default({});
1402
- var RawMemory = z4.object({ ompBackend: z4.enum(["off", "local"]).default("off") }).strict().default({});
1419
+ var RawMemory = z5.object({ ompBackend: z5.enum(["off", "local"]).default("off") }).strict().default({});
1403
1420
  var SKILL_SOURCE_SCOPES = ["shared", "instance"];
1404
1421
  var RawSkillSourceMetadata = {
1405
- owner: z4.string().min(1).optional(),
1406
- scope: z4.enum(SKILL_SOURCE_SCOPES).optional(),
1407
- requires: z4.array(z4.enum(CAPABILITY_IDS)).optional()
1422
+ owner: z5.string().min(1).optional(),
1423
+ scope: z5.enum(SKILL_SOURCE_SCOPES).optional(),
1424
+ requires: z5.array(z5.enum(CAPABILITY_IDS)).optional()
1408
1425
  };
1409
- var RawSkillSource = z4.union([
1410
- z4.object({
1411
- github: z4.string().min(1),
1412
- ref: z4.string().min(1).optional(),
1413
- path: z4.string().optional(),
1426
+ var RawSkillSource = z5.union([
1427
+ z5.object({
1428
+ github: z5.string().min(1),
1429
+ ref: z5.string().min(1).optional(),
1430
+ path: z5.string().optional(),
1414
1431
  ...RawSkillSourceMetadata
1415
1432
  }).strict(),
1416
- z4.object({ s3: z4.string().min(1), ...RawSkillSourceMetadata }).strict()
1433
+ z5.object({ s3: z5.string().min(1), ...RawSkillSourceMetadata }).strict()
1417
1434
  ]);
1418
- var RawSkills = z4.object({
1419
- sources: z4.array(RawSkillSource).optional(),
1420
- promoteTo: z4.string().min(1).optional()
1435
+ var RawSkills = z5.object({
1436
+ sources: z5.array(RawSkillSource).optional(),
1437
+ promoteTo: z5.string().min(1).optional()
1421
1438
  }).strict().default({});
1422
- var RawInstanceConfig = z4.object({
1423
- name: z4.string().min(1),
1424
- model: z4.string().regex(MODEL_RE, "model must be provider/modelId"),
1425
- admins: z4.array(z4.string().min(1)).min(1, "admins must list at least one Slack user id"),
1426
- instructions: z4.string().default(""),
1427
- timezone: z4.string().default("UTC").refine(isValidTimezone, (tz) => ({ message: `unknown timezone: ${tz}` })),
1439
+ var RawInstanceConfig = z5.object({
1440
+ name: z5.string().min(1),
1441
+ model: z5.string().regex(MODEL_RE, "model must be provider/modelId"),
1442
+ admins: z5.array(z5.string().min(1)).min(1, "admins must list at least one Slack user id"),
1443
+ instructions: z5.string().default(""),
1444
+ timezone: z5.string().default("UTC").refine(isValidTimezone, (tz) => ({ message: `unknown timezone: ${tz}` })),
1428
1445
  thinkingLevel: ThinkingLevel.optional(),
1429
1446
  skills: RawSkills,
1430
1447
  images: RawImages,
@@ -1501,24 +1518,24 @@ function parseInstanceConfig(yamlText) {
1501
1518
  }
1502
1519
  // ../core/src/company-capabilities.ts
1503
1520
  import { parse as parseYaml3 } from "yaml";
1504
- import { z as z5 } from "zod";
1521
+ import { z as z6 } from "zod";
1505
1522
  var COMPANY_CAPABILITY_DECLARATION_VERSION = 1;
1506
1523
  var COMPANY_CAPABILITY_DECLARATION_MAX_COUNT = CAPABILITY_IDS.length;
1507
1524
  var COMPANY_CAPABILITY_DESCRIPTION_MAX_CHARS = 280;
1508
1525
  var COMPANY_CAPABILITY_DECLARATION_MAX_BYTES = 16 * 1024;
1509
- var RawCompanyCapabilityDeclaration = z5.object({
1510
- id: z5.enum(CAPABILITY_IDS),
1511
- description: z5.string().min(1).max(COMPANY_CAPABILITY_DESCRIPTION_MAX_CHARS).refine((value) => value.trim() === value && !Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127), "description must not have surrounding whitespace or control characters")
1526
+ var RawCompanyCapabilityDeclaration = z6.object({
1527
+ id: z6.enum(CAPABILITY_IDS),
1528
+ description: z6.string().min(1).max(COMPANY_CAPABILITY_DESCRIPTION_MAX_CHARS).refine((value) => value.trim() === value && !Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127), "description must not have surrounding whitespace or control characters")
1512
1529
  }).strict();
1513
- var RawCompanyCapabilityDeclarations = z5.object({
1514
- version: z5.literal(COMPANY_CAPABILITY_DECLARATION_VERSION),
1515
- capabilities: z5.array(RawCompanyCapabilityDeclaration).max(COMPANY_CAPABILITY_DECLARATION_MAX_COUNT)
1530
+ var RawCompanyCapabilityDeclarations = z6.object({
1531
+ version: z6.literal(COMPANY_CAPABILITY_DECLARATION_VERSION),
1532
+ capabilities: z6.array(RawCompanyCapabilityDeclaration).max(COMPANY_CAPABILITY_DECLARATION_MAX_COUNT)
1516
1533
  }).strict().superRefine((document, ctx) => {
1517
1534
  const seen = new Set;
1518
1535
  for (const [index, declaration] of document.capabilities.entries()) {
1519
1536
  if (seen.has(declaration.id)) {
1520
1537
  ctx.addIssue({
1521
- code: z5.ZodIssueCode.custom,
1538
+ code: z6.ZodIssueCode.custom,
1522
1539
  path: ["capabilities", index, "id"],
1523
1540
  message: `duplicate capability declaration: ${declaration.id}`
1524
1541
  });
@@ -1585,13 +1602,13 @@ var LIVE_ENDPOINT_NAME = "live";
1585
1602
  // src/release/manifest.ts
1586
1603
  import { createHash as createHash3 } from "node:crypto";
1587
1604
  import { readFileSync as readFileSync2, statSync } from "node:fs";
1588
- import { z as z6 } from "zod";
1605
+ import { z as z7 } from "zod";
1589
1606
  var RELEASE_VERSION_RE = /^v\d+\.\d+\.\d+$/;
1590
- var Sha256 = z6.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase hex sha256");
1591
- var ReleaseArtifactSchema = z6.object({
1592
- key: z6.string().min(1),
1607
+ var Sha256 = z7.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase hex sha256");
1608
+ var ReleaseArtifactSchema = z7.object({
1609
+ key: z7.string().min(1),
1593
1610
  sha256: Sha256,
1594
- bytes: z6.number().int().nonnegative()
1611
+ bytes: z7.number().int().nonnegative()
1595
1612
  }).strict();
1596
1613
  var SIGNING_ALGORITHMS = [
1597
1614
  "RSASSA_PSS_SHA_256",
@@ -1604,21 +1621,21 @@ var SIGNING_ALGORITHMS = [
1604
1621
  "ECDSA_SHA_384",
1605
1622
  "ECDSA_SHA_512"
1606
1623
  ];
1607
- var ReleaseManifestSchema = z6.object({
1608
- version: z6.string().regex(RELEASE_VERSION_RE, "must be vMAJOR.MINOR.PATCH"),
1609
- gitCommit: z6.string().regex(/^[0-9a-f]{40}$/, "must be a full commit sha"),
1610
- createdAt: z6.string().datetime(),
1611
- agentImage: z6.object({
1612
- repository: z6.string().min(1),
1613
- tag: z6.string().min(1),
1614
- digest: z6.string().regex(/^sha256:[0-9a-f]{64}$/)
1624
+ var ReleaseManifestSchema = z7.object({
1625
+ version: z7.string().regex(RELEASE_VERSION_RE, "must be vMAJOR.MINOR.PATCH"),
1626
+ gitCommit: z7.string().regex(/^[0-9a-f]{40}$/, "must be a full commit sha"),
1627
+ createdAt: z7.string().datetime(),
1628
+ agentImage: z7.object({
1629
+ repository: z7.string().min(1),
1630
+ tag: z7.string().min(1),
1631
+ digest: z7.string().regex(/^sha256:[0-9a-f]{64}$/)
1615
1632
  }).strict(),
1616
- lambda: z6.record(ReleaseArtifactSchema),
1617
- skills: z6.object({ key: z6.string().min(1), sha256: Sha256 }).strict(),
1618
- signature: z6.object({
1619
- kmsKeyArn: z6.string().min(1),
1620
- algorithm: z6.enum(SIGNING_ALGORITHMS),
1621
- value: z6.string().min(1)
1633
+ lambda: z7.record(ReleaseArtifactSchema),
1634
+ skills: z7.object({ key: z7.string().min(1), sha256: Sha256 }).strict(),
1635
+ signature: z7.object({
1636
+ kmsKeyArn: z7.string().min(1),
1637
+ algorithm: z7.enum(SIGNING_ALGORITHMS),
1638
+ value: z7.string().min(1)
1622
1639
  }).strict().optional()
1623
1640
  }).strict();
1624
1641
  function parseManifest(json) {
@@ -24,7 +24,7 @@ import {
24
24
  scheduledJobBindings,
25
25
  slackCommandPrefixes,
26
26
  webhookJobBindings
27
- } from "./chunk-7rd8c1th.js";
27
+ } from "./chunk-g56mqyfx.js";
28
28
 
29
29
  // src/stacks/agent-stack.ts
30
30
  import * as cdk from "aws-cdk-lib";
@@ -1104,6 +1104,14 @@ class FoundationApi extends cdk2.Stack {
1104
1104
  `arn:${this.partition}:bedrock-agentcore:${this.region}:${this.account}:runtime/${names.runtimeName}*/runtime-endpoint/*`
1105
1105
  ]
1106
1106
  }));
1107
+ invokerRole.addToPolicy(new iam2.PolicyStatement({
1108
+ sid: "StopSingleUseRuntimeSessions",
1109
+ actions: ["bedrock-agentcore:StopRuntimeSession"],
1110
+ resources: [
1111
+ `arn:${this.partition}:bedrock-agentcore:${this.region}:${this.account}:runtime/${names.runtimeName}*`,
1112
+ `arn:${this.partition}:bedrock-agentcore:${this.region}:${this.account}:runtime/${names.runtimeName}*/runtime-endpoint/*`
1113
+ ]
1114
+ }));
1107
1115
  invokerRole.addToPolicy(new iam2.PolicyStatement({
1108
1116
  sid: "ReadRuntimeEndpointVersion",
1109
1117
  actions: ["bedrock-agentcore:GetAgentRuntimeEndpoint"],
@@ -1937,6 +1945,7 @@ function deployStatements(stack, names, targets, instance) {
1937
1945
  sid: "SmokeTestRuntime",
1938
1946
  actions: [
1939
1947
  "bedrock-agentcore:InvokeAgentRuntime",
1948
+ "bedrock-agentcore:StopRuntimeSession",
1940
1949
  "bedrock-agentcore:GetAgentRuntime",
1941
1950
  "bedrock-agentcore:ListAgentRuntimeVersions",
1942
1951
  "bedrock-agentcore:GetAgentRuntimeEndpoint",
package/dist/src/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  FoundationPipeline,
11
11
  NewsletterStack,
12
12
  deployStatements
13
- } from "../chunk-6v0ewr5b.js";
13
+ } from "../chunk-sgh3agpa.js";
14
14
  import {
15
15
  BUN_VERSION,
16
16
  LAMBDA_ENTRY_POINTS,
@@ -44,7 +44,7 @@ import {
44
44
  skillsKey,
45
45
  skipBundle,
46
46
  verifyManifest
47
- } from "../chunk-7rd8c1th.js";
47
+ } from "../chunk-g56mqyfx.js";
48
48
  import"../chunk-dz91avrb.js";
49
49
  export {
50
50
  BUILD_TIMEOUT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deployfoundation/foundation-deploy",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -214,6 +214,32 @@ export interface SmokeOptions {
214
214
  log?: (line: string) => void;
215
215
  }
216
216
 
217
+ /**
218
+ * End a smoke session once it has answered. Its id is minted per invoke and
219
+ * never used again, so otherwise the microVM idles out its whole idle timeout
220
+ * billed for its memory (~2 GB-hours each; a deploy runs three). Best-effort:
221
+ * a deployer without the permission still deploys.
222
+ */
223
+ async function stopSmokeSession(
224
+ runner: EndpointRunner,
225
+ opts: SmokeOptions,
226
+ log: (line: string) => void,
227
+ ): Promise<void> {
228
+ try {
229
+ await runner.capture([
230
+ "bedrock-agentcore",
231
+ "stop-runtime-session",
232
+ "--agent-runtime-arn",
233
+ opts.arn,
234
+ ...(opts.qualifier === undefined ? [] : ["--qualifier", opts.qualifier]),
235
+ "--runtime-session-id",
236
+ opts.sessionId,
237
+ ]);
238
+ } catch {
239
+ log(" (could not stop the smoke session; it ends at the idle timeout)");
240
+ }
241
+ }
242
+
217
243
  /** Invoke the runtime and assert the response says what a healthy runtime says. */
218
244
  export async function smokeInvoke(runner: EndpointRunner, opts: SmokeOptions): Promise<void> {
219
245
  const log = opts.log ?? ((line: string) => console.log(line));
@@ -250,6 +276,7 @@ export async function smokeInvoke(runner: EndpointRunner, opts: SmokeOptions): P
250
276
  } finally {
251
277
  await rm(dir, { recursive: true, force: true });
252
278
  }
279
+ await stopSmokeSession(runner, opts, log);
253
280
  log(` ${out.replaceAll("\n", "\n ")}`);
254
281
  for (const needle of opts.expect ?? SMOKE_EXPECT)
255
282
  if (!out.includes(needle)) throw new Error(`smoke test: response does not contain ${needle}`);
@@ -175,11 +175,13 @@ export function deployStatements(
175
175
  // `foundation-deploy post-deploy` (and `deploy`'s inline promotion): the
176
176
  // smoke test (`_smoke_test`, `_fs_probe`) against DEFAULT, then promotion
177
177
  // of the new version onto `live`, which CREATES `live` when an instance
178
- // brought up by `deploy` alone has none yet (`promoteEndpoint`).
178
+ // brought up by `deploy` alone has none yet (`promoteEndpoint`). Each
179
+ // smoke session is stopped once it answers: nothing invokes it again.
179
180
  new iam.PolicyStatement({
180
181
  sid: "SmokeTestRuntime",
181
182
  actions: [
182
183
  "bedrock-agentcore:InvokeAgentRuntime",
184
+ "bedrock-agentcore:StopRuntimeSession",
183
185
  "bedrock-agentcore:GetAgentRuntime",
184
186
  "bedrock-agentcore:ListAgentRuntimeVersions",
185
187
  "bedrock-agentcore:GetAgentRuntimeEndpoint",
@@ -529,6 +529,19 @@ export class FoundationApi extends cdk.Stack {
529
529
  ],
530
530
  }),
531
531
  );
532
+ // A routine fire's session is single-use: the invoker ends it as soon as
533
+ // the fire answers instead of paying for its memory through the idle
534
+ // timeout (`isSingleUseSession` in gateway/src/invoker.ts).
535
+ invokerRole.addToPolicy(
536
+ new iam.PolicyStatement({
537
+ sid: "StopSingleUseRuntimeSessions",
538
+ actions: ["bedrock-agentcore:StopRuntimeSession"],
539
+ resources: [
540
+ `arn:${this.partition}:bedrock-agentcore:${this.region}:${this.account}:runtime/${names.runtimeName}*`,
541
+ `arn:${this.partition}:bedrock-agentcore:${this.region}:${this.account}:runtime/${names.runtimeName}*/runtime-endpoint/*`,
542
+ ],
543
+ }),
544
+ );
532
545
  // Which version the endpoint serves: the invoker names it in the session
533
546
  // id so a thread moves off a pre-deploy microVM on its next turn.
534
547
  invokerRole.addToPolicy(