thinkwork-cli 0.13.0 → 0.13.1

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.
Files changed (74) hide show
  1. package/dist/{api-client-JUBCQQDF.js → api-client-4DZ266J5.js} +1 -1
  2. package/dist/{chunk-STY44FQO.js → chunk-Y2WHJECR.js} +44 -12
  3. package/dist/cli.js +36715 -12965
  4. package/dist/drizzle/0227_analyst_reader_role.sql +100 -38
  5. package/dist/drizzle/0230_analyst_rls.sql +183 -66
  6. package/dist/drizzle/0232_memory_source_thin_ledger.sql +176 -0
  7. package/dist/drizzle/0232_message_source_event_id.sql +9 -0
  8. package/dist/drizzle/0233_msteams_install_and_links.sql +151 -0
  9. package/dist/drizzle/0233_workflow_memory_stage_tokens.sql +20 -0
  10. package/dist/drizzle/0234_memory_claim_lifecycle.sql +160 -0
  11. package/dist/drizzle/0235_workflow_token_execution_claim.sql +34 -0
  12. package/dist/drizzle/0236_memory_retraction_fencing.sql +50 -0
  13. package/dist/drizzle/0237_memory_claims_active_value_uidx.sql +172 -0
  14. package/dist/drizzle/0238_memory_erase_generation_fence.sql +97 -0
  15. package/dist/drizzle/0239_canonical_entity_identity.sql +257 -0
  16. package/dist/drizzle/0240_memory_personal_scope.sql +45 -0
  17. package/dist/drizzle/0241_knowledge_base_document_manifest.sql +67 -0
  18. package/dist/drizzle/0242_search_queries.sql +28 -0
  19. package/dist/drizzle/0243_section_sources_thread_refs.sql +12 -0
  20. package/dist/drizzle/0244_system_automations_and_memory_stage_overrides.sql +45 -0
  21. package/dist/drizzle/0245_wiki_page_render.sql +21 -0
  22. package/dist/drizzle/0246_automation_workflow_ownership.sql +30 -0
  23. package/dist/drizzle/0247_capability_runtime.sql +402 -0
  24. package/dist/drizzle/0248_capability_private_interpreter.sql +13 -0
  25. package/dist/drizzle/0249_capability_headless_execution_evidence.sql +40 -0
  26. package/dist/drizzle/0250_kg_schema_extraction.sql +549 -0
  27. package/dist/drizzle/0251_agents_json_render_ui.sql +25 -0
  28. package/dist/drizzle/0251_brain_stragglers.sql +242 -0
  29. package/dist/drizzle/0252_drop_kg_and_straggler_compat_views.sql +100 -0
  30. package/dist/drizzle/0253_drop_brain_substrate.sql +91 -0
  31. package/dist/drizzle/0254_capability_autonomous_admission.sql +29 -0
  32. package/dist/drizzle/0255_capability_autonomous_approval_mode.sql +16 -0
  33. package/dist/drizzle/0256_agent_profile_manifest_authority.sql +18 -0
  34. package/dist/drizzle/0257_capability_approvals.sql +53 -0
  35. package/dist/drizzle/0258_pending_tool_approvals.sql +84 -0
  36. package/dist/drizzle/0259_tenant_capability_registry_trust.sql +19 -0
  37. package/dist/drizzle/0260_scheduled_jobs_run_as_user.sql +20 -0
  38. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +11 -0
  39. package/dist/terraform/examples/greenfield/main.tf +40 -0
  40. package/dist/terraform/modules/app/agentcore-admin/main.tf +341 -0
  41. package/dist/terraform/modules/app/agentcore-admin/outputs.tf +20 -0
  42. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +61 -0
  43. package/dist/terraform/modules/app/agentcore-harness/main.tf +101 -0
  44. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +9 -0
  45. package/dist/terraform/modules/app/agentcore-harness/variables.tf +25 -0
  46. package/dist/terraform/modules/app/agentcore-pi/main.tf +7 -0
  47. package/dist/terraform/modules/app/capability-broker/main.tf +393 -0
  48. package/dist/terraform/modules/app/capability-broker/outputs.tf +54 -0
  49. package/dist/terraform/modules/app/capability-broker/variables.tf +98 -0
  50. package/dist/terraform/modules/app/deployment-control-plane/runner.py +46 -5
  51. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +94 -0
  52. package/dist/terraform/modules/app/lambda-api/capability-search-alarms.tf +60 -0
  53. package/dist/terraform/modules/app/lambda-api/handlers.tf +361 -16
  54. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +131 -5
  55. package/dist/terraform/modules/app/lambda-api/main.tf +11 -0
  56. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +92 -0
  57. package/dist/terraform/modules/app/lambda-api/msteams-app-secrets.tf +46 -0
  58. package/dist/terraform/modules/app/lambda-api/variables.tf +92 -0
  59. package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/alarms.tf +47 -0
  60. package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/main.tf +43 -2
  61. package/dist/terraform/modules/foundation/cognito/main.tf +32 -8
  62. package/dist/terraform/modules/foundation/cognito/variables.tf +12 -0
  63. package/dist/terraform/modules/foundation/vpc/outputs.tf +5 -0
  64. package/dist/terraform/modules/thinkwork/main.tf +152 -5
  65. package/dist/terraform/modules/thinkwork/outputs.tf +55 -0
  66. package/dist/terraform/modules/thinkwork/variables.tf +42 -0
  67. package/dist/workspace-defaults/files/TOOLS.md +39 -0
  68. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +18 -0
  69. package/dist/workspace-defaults/files/agents/coding/INSTRUCTIONS.md +11 -0
  70. package/dist/workspace-defaults/files/agents/research/INSTRUCTIONS.md +12 -0
  71. package/dist/workspace-defaults/files/agents/reviewer/INSTRUCTIONS.md +15 -0
  72. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +6 -2
  73. package/package.json +4 -2
  74. /package/dist/workspace-defaults/files/{AGENTS.md → INSTRUCTIONS.md} +0 -0
@@ -0,0 +1,25 @@
1
+ variable "enabled" {
2
+ description = "Create the AgentCore Harness trial IAM surface (THINK-311 U4). Pure-IAM and inert — nothing invokes Harness until U5 — so it defaults on; flip off to remove the role without touching root variables."
3
+ type = bool
4
+ default = true
5
+ }
6
+
7
+ variable "stage" {
8
+ description = "Deployment stage (dev, prod, etc.)"
9
+ type = string
10
+ }
11
+
12
+ variable "region" {
13
+ description = "AWS region"
14
+ type = string
15
+ }
16
+
17
+ variable "account_id" {
18
+ description = "AWS account ID"
19
+ type = string
20
+ }
21
+
22
+ variable "bucket_name" {
23
+ description = "Stage workspace S3 bucket name — Harness microVMs read tenant skill sources under tenants/*"
24
+ type = string
25
+ }
@@ -26,6 +26,11 @@ locals {
26
26
  chat_agent_activity_fn_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:${local.chat_agent_activity_fn_name}"
27
27
  manifest_log_fn_name = "thinkwork-${var.stage}-api-manifest-log"
28
28
  manifest_log_fn_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:${local.manifest_log_fn_name}"
29
+ # THINK-280 U2 — capability control plane service (capability_search /
30
+ # connection_research Pi tools). Direct InvokeCommand, RequestResponse;
31
+ # no HTTP route exists for this Lambda.
32
+ capability_control_fn_name = "thinkwork-${var.stage}-api-capability-control-service"
33
+ capability_control_fn_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:${local.capability_control_fn_name}"
29
34
  # Release-backed deployments use an immutable, source-derived target tag so
30
35
  # changing the release image also changes aws_lambda_function.image_uri.
31
36
  # Reusing pi-latest lets Terraform miss the update even after ECR is retagged.
@@ -259,6 +264,7 @@ resource "aws_iam_role_policy" "agentcore_pi" {
259
264
  local.chat_agent_finalize_fn_arn,
260
265
  local.chat_agent_activity_fn_arn,
261
266
  local.manifest_log_fn_arn,
267
+ local.capability_control_fn_arn,
262
268
  ]
263
269
  },
264
270
  {
@@ -384,6 +390,7 @@ resource "aws_lambda_function" "agentcore_pi" {
384
390
  CHAT_AGENT_FINALIZE_FN_NAME = local.chat_agent_finalize_fn_name
385
391
  CHAT_AGENT_ACTIVITY_FN_NAME = local.chat_agent_activity_fn_name
386
392
  MANIFEST_LOG_FUNCTION_NAME = local.manifest_log_fn_name
393
+ CAPABILITY_CONTROL_FN_NAME = local.capability_control_fn_name
387
394
  HINDSIGHT_ENDPOINT = var.hindsight_endpoint
388
395
  # THINK-220 cutover flag — see the variable description.
389
396
  HINDSIGHT_DATABASE_NAME = var.hindsight_database_name
@@ -0,0 +1,393 @@
1
+ # Capability Broker (THINK-280 U3) — action-time policy, replay-safe PoP
2
+ # sessions, and evidence for the governed capability runtime.
3
+ #
4
+ # SHIP-INERT: gated by var.enabled (default false). Nothing here is created
5
+ # until U4 flips it on after the live negative-egress proof. The Lambda code
6
+ # itself also fails closed — its default authorization loader denies every
7
+ # call — so even an accidental enable dispatches no provider operation.
8
+ #
9
+ # THINK-144: this module owns a DEDICATED execute-api interface VPC endpoint
10
+ # with private DNS DISABLED. It is never added to the app VPC's shared
11
+ # okf_wiki_interface_endpoint_services list — that list's private DNS would
12
+ # capture every *.execute-api call in the VPC and 403 the stack's public HTTP
13
+ # APIs. The session bootstrap targets this endpoint's own DNS name instead.
14
+
15
+ locals {
16
+ enabled = var.enabled
17
+ count_enabled = var.enabled ? 1 : 0
18
+
19
+ name_prefix = "thinkwork-${var.stage}-capability-broker"
20
+ session_table_name = "thinkwork-${var.stage}-capability-broker-sessions"
21
+ broker_audience = "thinkwork-${var.stage}-capability-broker"
22
+ use_local_zips = var.lambda_zips_dir != ""
23
+ use_remote_artifacts = trimspace(var.lambda_artifact_bucket) != ""
24
+ broker_zip_local = "${var.lambda_zips_dir}/capability-broker.zip"
25
+ broker_artifact_s3key = "${var.lambda_artifact_prefix}/capability-broker.zip"
26
+ }
27
+
28
+ # --- Replay-safe session state (DynamoDB, conditional writes + TTL) ----------
29
+
30
+ resource "aws_dynamodb_table" "sessions" {
31
+ count = local.count_enabled
32
+
33
+ name = local.session_table_name
34
+ billing_mode = "PAY_PER_REQUEST"
35
+ hash_key = "pk"
36
+ range_key = "sk"
37
+
38
+ attribute {
39
+ name = "pk"
40
+ type = "S"
41
+ }
42
+ attribute {
43
+ name = "sk"
44
+ type = "S"
45
+ }
46
+
47
+ # Session + nonce records self-expire at session TTL (≤15 min). Epoch seconds.
48
+ ttl {
49
+ attribute_name = "ttl"
50
+ enabled = true
51
+ }
52
+
53
+ point_in_time_recovery {
54
+ enabled = true
55
+ }
56
+
57
+ tags = {
58
+ Name = local.session_table_name
59
+ Purpose = "capability-broker-sessions"
60
+ }
61
+ }
62
+
63
+ # --- Broker Lambda execution role -------------------------------------------
64
+
65
+ data "aws_iam_policy_document" "broker_assume" {
66
+ statement {
67
+ actions = ["sts:AssumeRole"]
68
+ principals {
69
+ type = "Service"
70
+ identifiers = ["lambda.amazonaws.com"]
71
+ }
72
+ }
73
+ }
74
+
75
+ resource "aws_iam_role" "broker" {
76
+ count = local.count_enabled
77
+ name = "${local.name_prefix}-role"
78
+ assume_role_policy = data.aws_iam_policy_document.broker_assume.json
79
+ }
80
+
81
+ data "aws_iam_policy_document" "broker_policy" {
82
+ count = local.count_enabled
83
+
84
+ # Session state: conditional reads/writes only, scoped to the one table.
85
+ statement {
86
+ sid = "SessionState"
87
+ actions = [
88
+ "dynamodb:GetItem",
89
+ "dynamodb:PutItem",
90
+ "dynamodb:UpdateItem",
91
+ "dynamodb:DeleteItem",
92
+ "dynamodb:Query",
93
+ ]
94
+ resources = [aws_dynamodb_table.sessions[0].arn]
95
+ }
96
+
97
+ # Evidence: append capability_broker_calls rows via the RDS Data API on the
98
+ # existing Aurora cluster (same credential-rotation surface as graphql-http).
99
+ dynamic "statement" {
100
+ for_each = var.db_cluster_arn != "" ? [1] : []
101
+ content {
102
+ sid = "EvidenceDataApi"
103
+ actions = ["rds-data:ExecuteStatement", "rds-data:BatchExecuteStatement"]
104
+ resources = [var.db_cluster_arn]
105
+ }
106
+ }
107
+
108
+ dynamic "statement" {
109
+ for_each = var.db_secret_arn != "" ? [1] : []
110
+ content {
111
+ sid = "EvidenceDbSecret"
112
+ actions = ["secretsmanager:GetSecretValue"]
113
+ resources = [var.db_secret_arn]
114
+ }
115
+ }
116
+
117
+ # VPC ENI management for the in-VPC Lambda.
118
+ statement {
119
+ sid = "VpcEni"
120
+ actions = [
121
+ "ec2:CreateNetworkInterface",
122
+ "ec2:DescribeNetworkInterfaces",
123
+ "ec2:DeleteNetworkInterface",
124
+ "ec2:AssignPrivateIpAddresses",
125
+ "ec2:UnassignPrivateIpAddresses",
126
+ ]
127
+ resources = ["*"]
128
+ }
129
+
130
+ statement {
131
+ sid = "Logs"
132
+ actions = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
133
+ resources = ["arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/lambda/${local.name_prefix}*"]
134
+ }
135
+
136
+ statement {
137
+ sid = "Xray"
138
+ actions = ["xray:PutTraceSegments", "xray:PutTelemetryRecords"]
139
+ resources = ["*"]
140
+ }
141
+ }
142
+
143
+ resource "aws_iam_role_policy" "broker" {
144
+ count = local.count_enabled
145
+ name = "${local.name_prefix}-policy"
146
+ role = aws_iam_role.broker[0].id
147
+ policy = data.aws_iam_policy_document.broker_policy[0].json
148
+ }
149
+
150
+ # --- Broker Lambda security group (egress only) ------------------------------
151
+
152
+ resource "aws_security_group" "broker_lambda" {
153
+ count = local.count_enabled
154
+ name = "${local.name_prefix}-lambda"
155
+ description = "Capability broker Lambda egress"
156
+ vpc_id = var.vpc_id
157
+
158
+ egress {
159
+ from_port = 0
160
+ to_port = 0
161
+ protocol = "-1"
162
+ cidr_blocks = ["0.0.0.0/0"]
163
+ description = "Egress to Aurora Data API, DynamoDB, Secrets Manager, provider endpoints"
164
+ }
165
+
166
+ tags = { Name = "${local.name_prefix}-lambda" }
167
+ }
168
+
169
+ resource "aws_cloudwatch_log_group" "broker" {
170
+ count = local.count_enabled
171
+ name = "/aws/lambda/${local.name_prefix}"
172
+ retention_in_days = var.log_retention_days
173
+ }
174
+
175
+ resource "aws_lambda_function" "broker" {
176
+ count = local.count_enabled
177
+ function_name = local.name_prefix
178
+ role = aws_iam_role.broker[0].arn
179
+ runtime = var.lambda_runtime
180
+ handler = "index.handler"
181
+ timeout = 30
182
+ memory_size = 512
183
+
184
+ filename = local.use_local_zips ? local.broker_zip_local : null
185
+ source_code_hash = local.use_local_zips ? filebase64sha256(local.broker_zip_local) : null
186
+ s3_bucket = local.use_remote_artifacts ? var.lambda_artifact_bucket : null
187
+ s3_key = local.use_remote_artifacts ? local.broker_artifact_s3key : null
188
+
189
+ vpc_config {
190
+ subnet_ids = var.subnet_ids
191
+ security_group_ids = [aws_security_group.broker_lambda[0].id]
192
+ }
193
+
194
+ environment {
195
+ variables = {
196
+ CAPABILITY_BROKER_SESSION_TABLE = aws_dynamodb_table.sessions[0].name
197
+ CAPABILITY_BROKER_AUDIENCE = local.broker_audience
198
+ # getDb() (@thinkwork/database-pg) MUST get DATABASE_URL, not just
199
+ # DATABASE_SECRET_ARN: the secret-only path returns an async Proxy whose
200
+ # `db.select()` resolves to a Promise, which breaks Drizzle's fluent
201
+ # `.select().from().where()` chaining (`.from is not a function`) — the
202
+ # cause of the loader's `authorization reload failed`. DATABASE_URL takes
203
+ # the synchronous pool path, matching every api handler's common_env.
204
+ DATABASE_URL = "postgresql://${var.db_username}:${urlencode(var.db_password)}@${var.db_cluster_endpoint}:5432/${var.database_name}?sslmode=no-verify"
205
+ DATABASE_SECRET_ARN = var.db_secret_arn
206
+ DATABASE_HOST = var.db_cluster_endpoint
207
+ }
208
+ }
209
+
210
+ tracing_config {
211
+ mode = "Active"
212
+ }
213
+
214
+ depends_on = [aws_cloudwatch_log_group.broker]
215
+ }
216
+
217
+ # --- Private REST API Gateway fronting the broker ----------------------------
218
+ #
219
+ # PRIVATE endpoint: reachable ONLY through the dedicated execute-api VPCE below.
220
+ # The resource policy denies anything not arriving via that endpoint.
221
+
222
+ resource "aws_api_gateway_rest_api" "broker" {
223
+ count = local.count_enabled
224
+ name = local.name_prefix
225
+
226
+ endpoint_configuration {
227
+ types = ["PRIVATE"]
228
+ vpc_endpoint_ids = [aws_vpc_endpoint.broker_execute_api[0].id]
229
+ }
230
+
231
+ # Only traffic arriving through our own VPCE is allowed; everything else 403s.
232
+ policy = jsonencode({
233
+ Version = "2012-10-17"
234
+ Statement = [
235
+ {
236
+ Effect = "Deny"
237
+ Principal = "*"
238
+ Action = "execute-api:Invoke"
239
+ Resource = "arn:aws:execute-api:${var.region}:${var.account_id}:*/*/*/*"
240
+ Condition = {
241
+ StringNotEquals = { "aws:SourceVpce" = aws_vpc_endpoint.broker_execute_api[0].id }
242
+ }
243
+ },
244
+ {
245
+ Effect = "Allow"
246
+ Principal = "*"
247
+ Action = "execute-api:Invoke"
248
+ Resource = "arn:aws:execute-api:${var.region}:${var.account_id}:*/*/*/*"
249
+ },
250
+ ]
251
+ })
252
+ }
253
+
254
+ resource "aws_api_gateway_resource" "broker_proxy" {
255
+ count = local.count_enabled
256
+ rest_api_id = aws_api_gateway_rest_api.broker[0].id
257
+ parent_id = aws_api_gateway_rest_api.broker[0].root_resource_id
258
+ path_part = "{proxy+}"
259
+ }
260
+
261
+ resource "aws_api_gateway_method" "broker_any" {
262
+ count = local.count_enabled
263
+ rest_api_id = aws_api_gateway_rest_api.broker[0].id
264
+ resource_id = aws_api_gateway_resource.broker_proxy[0].id
265
+ http_method = "ANY"
266
+ authorization = "NONE"
267
+ }
268
+
269
+ resource "aws_api_gateway_integration" "broker" {
270
+ count = local.count_enabled
271
+ rest_api_id = aws_api_gateway_rest_api.broker[0].id
272
+ resource_id = aws_api_gateway_resource.broker_proxy[0].id
273
+ http_method = aws_api_gateway_method.broker_any[0].http_method
274
+ integration_http_method = "POST"
275
+ type = "AWS_PROXY"
276
+ uri = aws_lambda_function.broker[0].invoke_arn
277
+ }
278
+
279
+ resource "aws_lambda_permission" "broker_apigw" {
280
+ count = local.count_enabled
281
+ statement_id = "AllowBrokerPrivateApiInvoke"
282
+ action = "lambda:InvokeFunction"
283
+ function_name = aws_lambda_function.broker[0].function_name
284
+ principal = "apigateway.amazonaws.com"
285
+ source_arn = "${aws_api_gateway_rest_api.broker[0].execution_arn}/*/*/*"
286
+ }
287
+
288
+ resource "aws_api_gateway_deployment" "broker" {
289
+ count = local.count_enabled
290
+ rest_api_id = aws_api_gateway_rest_api.broker[0].id
291
+
292
+ triggers = {
293
+ redeploy = sha1(jsonencode([
294
+ aws_api_gateway_resource.broker_proxy[0].id,
295
+ aws_api_gateway_method.broker_any[0].id,
296
+ aws_api_gateway_integration.broker[0].id,
297
+ aws_api_gateway_rest_api.broker[0].policy,
298
+ ]))
299
+ }
300
+
301
+ lifecycle {
302
+ create_before_destroy = true
303
+ }
304
+ depends_on = [aws_api_gateway_integration.broker]
305
+ }
306
+
307
+ resource "aws_api_gateway_stage" "broker" {
308
+ count = local.count_enabled
309
+ rest_api_id = aws_api_gateway_rest_api.broker[0].id
310
+ deployment_id = aws_api_gateway_deployment.broker[0].id
311
+ stage_name = var.stage
312
+ }
313
+
314
+ # --- Dedicated execute-api VPC endpoint (PRIVATE DNS DISABLED — THINK-144) ----
315
+
316
+ resource "aws_security_group" "broker_vpce" {
317
+ count = local.count_enabled
318
+ name = "${local.name_prefix}-vpce"
319
+ description = "Capability broker execute-api VPCE (broker Lambda + capability-private interpreter subnet only)"
320
+ vpc_id = var.vpc_id
321
+
322
+ ingress {
323
+ from_port = 443
324
+ to_port = 443
325
+ protocol = "tcp"
326
+ cidr_blocks = [var.vpc_cidr_block]
327
+ description = "HTTPS from in-VPC PoP callers (broker Lambda self-calls + U4 capability-private interpreter)"
328
+ }
329
+
330
+ tags = { Name = "${local.name_prefix}-vpce" }
331
+ }
332
+
333
+ resource "aws_vpc_endpoint" "broker_execute_api" {
334
+ count = local.count_enabled
335
+
336
+ vpc_id = var.vpc_id
337
+ service_name = "com.amazonaws.${var.region}.execute-api"
338
+ vpc_endpoint_type = "Interface"
339
+ subnet_ids = var.subnet_ids
340
+ security_group_ids = [aws_security_group.broker_vpce[0].id]
341
+
342
+ # CRITICAL (THINK-144): private DNS OFF. Callers target the endpoint-specific
343
+ # DNS name (surfaced as an output), never the regional execute-api domain, so
344
+ # this endpoint cannot capture the stack's public HTTP API traffic.
345
+ private_dns_enabled = false
346
+
347
+ tags = {
348
+ Name = "${local.name_prefix}-execute-api-vpce"
349
+ Purpose = "capability-broker-private-ingress"
350
+ }
351
+ }
352
+
353
+ # --- Capability-private interpreter SG (no NAT; broker VPCE only) — U4 --------
354
+ #
355
+ # The U4 capability-private VPC-mode Code Interpreter attaches to var.subnet_ids
356
+ # (the no-NAT private egress subnets) under THIS security group. Its ONLY egress
357
+ # is 443 to the broker execute-api VPCE SG plus DNS to the VPC resolver — there
358
+ # is no NAT and no 0.0.0.0/0 route, so direct GitHub / Secrets Manager / internet
359
+ # access does not exist (AE5 negative-egress boundary). The broker_vpce ingress
360
+ # above already admits 443 from the VPC CIDR, which covers these subnets.
361
+ resource "aws_security_group" "capability_private_interpreter" {
362
+ count = local.count_enabled
363
+ name = "${local.name_prefix}-cappriv-interpreter"
364
+ description = "Capability-private Code Interpreter egress: broker execute-api VPCE + DNS only"
365
+ vpc_id = var.vpc_id
366
+
367
+ # execute-api VPCE only (reach the private broker API).
368
+ egress {
369
+ from_port = 443
370
+ to_port = 443
371
+ protocol = "tcp"
372
+ security_groups = [aws_security_group.broker_vpce[0].id]
373
+ description = "HTTPS to the broker execute-api VPCE"
374
+ }
375
+
376
+ # DNS to the in-VPC resolver so the endpoint-specific VPCE hostname resolves.
377
+ egress {
378
+ from_port = 53
379
+ to_port = 53
380
+ protocol = "udp"
381
+ cidr_blocks = [var.vpc_cidr_block]
382
+ description = "DNS (UDP) to the VPC resolver"
383
+ }
384
+ egress {
385
+ from_port = 53
386
+ to_port = 53
387
+ protocol = "tcp"
388
+ cidr_blocks = [var.vpc_cidr_block]
389
+ description = "DNS (TCP) to the VPC resolver"
390
+ }
391
+
392
+ tags = { Name = "${local.name_prefix}-cappriv-interpreter" }
393
+ }
@@ -0,0 +1,54 @@
1
+ # Consumed by the API-side broker-session opener to build the sandbox
2
+ # SessionBootstrap. All null when the module is disabled (ship-inert).
3
+
4
+ output "enabled" {
5
+ value = local.enabled
6
+ }
7
+
8
+ output "session_table_name" {
9
+ description = "DynamoDB session table (CAPABILITY_BROKER_SESSION_TABLE)."
10
+ value = local.enabled ? aws_dynamodb_table.sessions[0].name : ""
11
+ }
12
+
13
+ output "session_table_arn" {
14
+ description = "ARN of the broker session table — granted to the lambda-api role so routine-exec-git can mint/advance PoP sessions. Empty when disabled."
15
+ value = local.enabled ? aws_dynamodb_table.sessions[0].arn : ""
16
+ }
17
+
18
+ output "broker_audience" {
19
+ description = "Audience the PoP session signatures are bound to (CAPABILITY_BROKER_AUDIENCE)."
20
+ value = local.enabled ? local.broker_audience : ""
21
+ }
22
+
23
+ output "private_api_id" {
24
+ description = "Private REST API id the sandbox targets through the VPCE (CAPABILITY_BROKER_API_ID)."
25
+ value = local.enabled ? aws_api_gateway_rest_api.broker[0].id : ""
26
+ }
27
+
28
+ output "execute_api_vpce_id" {
29
+ value = local.enabled ? aws_vpc_endpoint.broker_execute_api[0].id : ""
30
+ }
31
+
32
+ output "execute_api_vpce_dns_name" {
33
+ description = "Endpoint-specific execute-api DNS name (private DNS is OFF). The session bootstrap uses this exact host — CAPABILITY_BROKER_VPCE_DNS."
34
+ value = local.enabled ? tolist(aws_vpc_endpoint.broker_execute_api[0].dns_entry)[0].dns_name : ""
35
+ }
36
+
37
+ output "broker_function_name" {
38
+ value = local.enabled ? aws_lambda_function.broker[0].function_name : ""
39
+ }
40
+
41
+ # --- U4: capability-private interpreter VPC placement ------------------------
42
+ # Consumed by the agentcore-admin Lambda (CAPABILITY_PRIVATE_SUBNET_IDS /
43
+ # CAPABILITY_PRIVATE_SECURITY_GROUP_IDS). Empty lists when disabled so the
44
+ # Lambda skips capability-private provisioning entirely.
45
+
46
+ output "capability_private_interpreter_subnet_ids" {
47
+ description = "No-NAT subnets the capability-private interpreter attaches to (same egress subnets as the broker)."
48
+ value = local.enabled ? var.subnet_ids : []
49
+ }
50
+
51
+ output "capability_private_interpreter_security_group_id" {
52
+ description = "Egress-only SG (broker execute-api VPCE + DNS only) for the capability-private interpreter. Empty when disabled."
53
+ value = local.enabled ? aws_security_group.capability_private_interpreter[0].id : ""
54
+ }
@@ -0,0 +1,98 @@
1
+ variable "enabled" {
2
+ description = "Master switch. When false the module creates nothing (ship-inert: U3 lands the broker code + this module gated off; U4 flips it on after the negative-egress proof)."
3
+ type = bool
4
+ default = false
5
+ }
6
+
7
+ variable "stage" {
8
+ type = string
9
+ }
10
+
11
+ variable "account_id" {
12
+ type = string
13
+ }
14
+
15
+ variable "region" {
16
+ type = string
17
+ }
18
+
19
+ # --- VPC placement (broker Lambda egress subnets + the dedicated VPCE) -------
20
+
21
+ variable "vpc_id" {
22
+ type = string
23
+ }
24
+
25
+ variable "vpc_cidr_block" {
26
+ type = string
27
+ }
28
+
29
+ variable "subnet_ids" {
30
+ description = "Private egress subnets the broker Lambda and its dedicated execute-api VPC endpoint attach to."
31
+ type = list(string)
32
+ default = []
33
+ }
34
+
35
+ # --- Evidence datastore (Aurora, RDS Data API — same cluster as graphql-http)-
36
+
37
+ variable "db_cluster_arn" {
38
+ type = string
39
+ default = ""
40
+ }
41
+
42
+ variable "db_secret_arn" {
43
+ type = string
44
+ default = ""
45
+ }
46
+
47
+ variable "db_cluster_endpoint" {
48
+ description = "Aurora writer endpoint hostname — DATABASE_HOST for getDb() (evidence store + authorization loader)."
49
+ type = string
50
+ default = ""
51
+ }
52
+
53
+ variable "db_username" {
54
+ description = "Aurora master username for the broker's DATABASE_URL."
55
+ type = string
56
+ default = "thinkwork_admin"
57
+ }
58
+
59
+ variable "db_password" {
60
+ description = "Aurora master password for the broker's DATABASE_URL (synchronous getDb() path)."
61
+ type = string
62
+ default = ""
63
+ sensitive = true
64
+ }
65
+
66
+ variable "database_name" {
67
+ description = "Aurora database name for the broker's DATABASE_URL."
68
+ type = string
69
+ default = "thinkwork"
70
+ }
71
+
72
+ # --- Lambda artifact source (mirrors lambda-api's dual local/remote pattern) -
73
+
74
+ variable "lambda_zips_dir" {
75
+ description = "Local path holding dist/lambdas/*.zip for local deploys. Empty when using remote S3 artifacts."
76
+ type = string
77
+ default = ""
78
+ }
79
+
80
+ variable "lambda_artifact_bucket" {
81
+ type = string
82
+ default = ""
83
+ }
84
+
85
+ variable "lambda_artifact_prefix" {
86
+ type = string
87
+ default = ""
88
+ }
89
+
90
+ variable "lambda_runtime" {
91
+ type = string
92
+ default = "nodejs22.x"
93
+ }
94
+
95
+ variable "log_retention_days" {
96
+ type = number
97
+ default = 30
98
+ }
@@ -2075,6 +2075,35 @@ def release_runtime_image(name):
2075
2075
  return ""
2076
2076
 
2077
2077
 
2078
+ def resolve_agentcore_pi_source_image_uri(payload):
2079
+ operation = payload.get("operation") if isinstance(payload.get("operation"), dict) else {}
2080
+ action = str(
2081
+ safe_get(operation, "action", default=safe_get(payload, "action", "phase", default=""))
2082
+ ).lower()
2083
+ kind = str(safe_get(operation, "kind", default="")).lower()
2084
+ is_customer_update = action == "update" or kind == "foundation"
2085
+ explicit = safe_get(payload, "agentcorePiSourceImageUri", default="")
2086
+ if explicit:
2087
+ if is_customer_update:
2088
+ account_id = safe_get(payload, "awsAccountId", default="")
2089
+ region = safe_get(payload, "awsRegion", default=os.environ.get("AWS_REGION", ""))
2090
+ customer_registry = f"{account_id}.dkr.ecr.{region}.amazonaws.com/"
2091
+ if not account_id or not region or not explicit.startswith(customer_registry):
2092
+ raise RuntimeError(
2093
+ "Customer foundation updates require agentcorePiSourceImageUri "
2094
+ "to reference the customer-owned ECR registry."
2095
+ )
2096
+ return explicit
2097
+
2098
+ if is_customer_update:
2099
+ raise RuntimeError(
2100
+ "Customer foundation updates require an explicit customer-ECR "
2101
+ "agentcorePiSourceImageUri; refusing to fall back to the release registry."
2102
+ )
2103
+
2104
+ return release_runtime_image("agentcore-pi-amd64")
2105
+
2106
+
2078
2107
  def release_git_sha():
2079
2108
  if not MANIFEST.exists():
2080
2109
  return ""
@@ -3053,6 +3082,16 @@ def write_runner_files(payload, runner_secrets):
3053
3082
  "enableHindsight",
3054
3083
  default=False,
3055
3084
  ),
3085
+ # Analyst data-path Lambda VPC egress (stable NAT EIP for external
3086
+ # database IP allowlists). Per-environment infra posture, so the
3087
+ # durable home is the runner-secrets document; a payload boolean
3088
+ # can override per-deploy.
3089
+ "analyst_lambda_vpc_egress": safe_get_bool(
3090
+ runner_secrets,
3091
+ reviewed_payload,
3092
+ "analystLambdaVpcEgress",
3093
+ default=False,
3094
+ ),
3056
3095
  # THINK-220: dedicated Hindsight database on the stage cluster.
3057
3096
  # Empty keeps the legacy hindsight-schema layout; set (e.g.
3058
3097
  # "thinkwork_hindsight") points the Hindsight service and platform
@@ -3161,11 +3200,7 @@ def write_runner_files(payload, runner_secrets):
3161
3200
  "deployment_evidence_bucket": os.environ.get("THINKWORK_EVIDENCE_BUCKET", ""),
3162
3201
  "deployment_terraform_module_source": terraform_module_source,
3163
3202
  "deployment_terraform_module_version": terraform_module_version,
3164
- "agentcore_pi_source_image_uri": safe_get(
3165
- payload,
3166
- "agentcorePiSourceImageUri",
3167
- default=release_runtime_image("agentcore-pi-amd64"),
3168
- ),
3203
+ "agentcore_pi_source_image_uri": resolve_agentcore_pi_source_image_uri(payload),
3169
3204
  }
3170
3205
  enforce_customer_domain_preservation(
3171
3206
  current_outputs,
@@ -3262,6 +3297,11 @@ variable "enable_hindsight" {{
3262
3297
  type = bool
3263
3298
  }}
3264
3299
 
3300
+ variable "analyst_lambda_vpc_egress" {{
3301
+ type = bool
3302
+ default = false
3303
+ }}
3304
+
3265
3305
  variable "hindsight_database_name" {{
3266
3306
  type = string
3267
3307
  default = ""
@@ -3655,6 +3695,7 @@ module "thinkwork" {{
3655
3695
  agentcore_pi_source_image_uri = var.agentcore_pi_source_image_uri
3656
3696
 
3657
3697
  enable_hindsight = var.enable_hindsight
3698
+ analyst_lambda_vpc_egress = var.analyst_lambda_vpc_egress
3658
3699
  hindsight_database_name = var.hindsight_database_name
3659
3700
  enable_workspace_orchestration = true
3660
3701