thinkwork-cli 0.8.2 → 0.9.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 (57) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +18 -2
  3. package/dist/cli.js +3004 -215
  4. package/dist/terraform/examples/greenfield/main.tf +325 -19
  5. package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
  6. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
  7. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
  8. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
  9. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
  10. package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
  11. package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
  12. package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
  13. package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
  14. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
  15. package/dist/terraform/modules/app/agentcore-runtime/main.tf +204 -4
  16. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
  17. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
  18. package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
  19. package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
  20. package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
  21. package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
  22. package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
  23. package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
  24. package/dist/terraform/modules/app/lambda-api/handlers.tf +1557 -42
  25. package/dist/terraform/modules/app/lambda-api/main.tf +299 -15
  26. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
  27. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
  28. package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
  29. package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
  30. package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
  31. package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
  32. package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
  33. package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
  34. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
  35. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
  36. package/dist/terraform/modules/app/static-site/main.tf +146 -5
  37. package/dist/terraform/modules/app/www-dns/main.tf +118 -15
  38. package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
  39. package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
  40. package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
  41. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
  42. package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
  43. package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
  44. package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
  45. package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
  46. package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
  47. package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
  48. package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
  49. package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
  50. package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
  51. package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
  52. package/dist/terraform/modules/foundation/cognito/variables.tf +5 -2
  53. package/dist/terraform/modules/thinkwork/main.tf +439 -21
  54. package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
  55. package/dist/terraform/modules/thinkwork/variables.tf +165 -6
  56. package/dist/terraform/schema.graphql +45 -0
  57. package/package.json +15 -14
@@ -10,7 +10,15 @@
10
10
  ################################################################################
11
11
 
12
12
  locals {
13
- bucket_name = var.bucket_name != "" ? var.bucket_name : "thinkwork-${var.stage}-storage"
13
+ bucket_name = var.bucket_name != "" ? var.bucket_name : "thinkwork-${var.stage}-storage"
14
+ backups_bucket_name = "thinkwork-${var.stage}-backups"
15
+ compliance_anchor_bucket_name = "thinkwork-${var.stage}-compliance-anchors"
16
+ compliance_exports_bucket_name = "thinkwork-${var.stage}-compliance-exports"
17
+ computer_task_subnet_ids = (
18
+ length(module.vpc.public_subnet_ids) > 0
19
+ ? module.vpc.public_subnet_ids
20
+ : module.vpc.private_subnet_ids
21
+ )
14
22
 
15
23
  # Hindsight is an optional add-on. Preferred toggle: var.enable_hindsight.
16
24
  # For one release we also honor the legacy var.memory_engine == "hindsight"
@@ -76,15 +84,28 @@ module "cognito" {
76
84
  google_oauth_client_secret = var.google_oauth_client_secret
77
85
  pre_signup_lambda_zip = var.pre_signup_lambda_zip
78
86
 
87
+ # Single ThinkworkAdmin Cognito client serves both admin and computer SPAs.
88
+ # apps/computer reuses this client by design — same humans, same tenant
89
+ # invitations, single sign-in across both surfaces. Each origin (admin
90
+ # distribution, admin custom domain, computer distribution, computer custom
91
+ # domain, plus localhost dev for both) needs both bare and /auth/callback
92
+ # entries because the OAuth flow lands on /auth/callback and the SPA's
93
+ # post-OAuth redirect lands on the bare origin.
79
94
  admin_callback_urls = concat(
80
95
  var.admin_callback_urls,
81
96
  ["https://${module.admin_site.distribution_domain}", "https://${module.admin_site.distribution_domain}/auth/callback"],
82
- var.admin_domain != "" ? ["https://${var.admin_domain}", "https://${var.admin_domain}/auth/callback"] : []
97
+ var.admin_domain != "" ? ["https://${var.admin_domain}", "https://${var.admin_domain}/auth/callback"] : [],
98
+ ["https://${module.computer_site.distribution_domain}", "https://${module.computer_site.distribution_domain}/auth/callback"],
99
+ var.computer_domain != "" ? ["https://${var.computer_domain}", "https://${var.computer_domain}/auth/callback"] : [],
100
+ ["http://localhost:5180", "http://localhost:5180/auth/callback"]
83
101
  )
84
102
  admin_logout_urls = concat(
85
103
  var.admin_logout_urls,
86
104
  ["https://${module.admin_site.distribution_domain}"],
87
- var.admin_domain != "" ? ["https://${var.admin_domain}"] : []
105
+ var.admin_domain != "" ? ["https://${var.admin_domain}"] : [],
106
+ ["https://${module.computer_site.distribution_domain}"],
107
+ var.computer_domain != "" ? ["https://${var.computer_domain}"] : [],
108
+ ["http://localhost:5180"]
88
109
  )
89
110
  mobile_callback_urls = var.mobile_callback_urls
90
111
  mobile_logout_urls = var.mobile_logout_urls
@@ -107,6 +128,60 @@ module "s3" {
107
128
  bucket_name = local.bucket_name
108
129
  }
109
130
 
131
+ module "s3_backups" {
132
+ source = "../data/s3-backups-bucket"
133
+
134
+ stage = var.stage
135
+ bucket_name = local.backups_bucket_name
136
+ }
137
+
138
+ # Phase 3 U7 — WORM-protected S3 bucket for SOC2 Type 1 tamper-evident audit
139
+ # anchoring. Inert in this PR: the IAM role exists but no Lambda assumes it
140
+ # until U8a (master plan Decision #9 — inert→live seam swap). The bucket
141
+ # itself is fully provisioned (Object Lock enabled at create time, KMS-
142
+ # encrypted, lifecycle to Glacier IR at 90 days, deny-DeleteObject bucket
143
+ # policy). See `terraform/modules/data/compliance-audit-bucket/README.md`.
144
+ module "compliance_anchors" {
145
+ source = "../data/compliance-audit-bucket"
146
+
147
+ stage = var.stage
148
+ account_id = var.account_id
149
+ region = var.region
150
+ bucket_name = local.compliance_anchor_bucket_name
151
+ kms_key_arn = module.kms.key_arn
152
+ mode = var.compliance_anchor_object_lock_mode
153
+ retention_days = var.compliance_anchor_retention_days
154
+
155
+ # Phase 3 U8a — anchor Lambda's IAM role gets `secretsmanager:GetSecretValue`
156
+ # on these two compliance secrets (anchor connects as compliance_reader for
157
+ # SELECT, compliance_drainer for tenant_anchor_state UPDATE).
158
+ compliance_reader_secret_arn = module.database.compliance_reader_secret_arn
159
+ compliance_drainer_secret_arn = module.database.compliance_drainer_secret_arn
160
+ }
161
+
162
+ # Phase 3 U11.U2 — Compliance exports bucket + runner IAM role.
163
+ #
164
+ # Ephemeral S3 bucket with 7-day lifecycle expiration. NOT Object Lock
165
+ # — exports are derivable from compliance.audit_events; the bucket is
166
+ # delivery plumbing, not the system of record. The runner Lambda assumes
167
+ # `module.compliance_exports.runner_role_arn` and writes CSV/NDJSON
168
+ # artifacts under any key (no per-prefix grant needed). U11.U2 ships the
169
+ # function with a stub body; U11.U3 swaps in the live runner.
170
+ module "compliance_exports" {
171
+ source = "../data/compliance-exports-bucket"
172
+
173
+ stage = var.stage
174
+ account_id = var.account_id
175
+ region = var.region
176
+ bucket_name = local.compliance_exports_bucket_name
177
+ # Runner Lambda reads writer-pool DB credentials at module-load to
178
+ # construct the Aurora connection string for INSERT/UPDATE on
179
+ # compliance.export_jobs and SELECT on compliance.audit_events.
180
+ # Without this grant the runner throws AccessDenied at first SQS
181
+ # invocation (deploy run 25561658625 failed the smoke gate this way).
182
+ database_secret_arn = module.database.graphql_db_secret_arn
183
+ }
184
+
110
185
  module "database" {
111
186
  source = "../data/aurora-postgres"
112
187
 
@@ -124,6 +199,15 @@ module "database" {
124
199
 
125
200
  database_name = var.database_name
126
201
  database_engine = var.database_engine
202
+
203
+ # Enables the `aws_s3` Aurora extension and attaches an IAM role that can
204
+ # PutObject into the backups bucket's pre-drop/* prefix. Used by
205
+ # destructive migrations (e.g. U5 of the thread-detail cleanup plan) to
206
+ # snapshot row data before DROP TABLE. `enable_aws_s3` is the plan-time
207
+ # gate (the bucket's ARN is known-after-apply on greenfield, so it can't
208
+ # drive `count` directly); the ARN still feeds the IAM policy body.
209
+ backups_bucket_arn = module.s3_backups.bucket_arn
210
+ enable_aws_s3 = var.database_engine == "aurora-serverless"
127
211
  }
128
212
 
129
213
  module "bedrock_kb" {
@@ -153,6 +237,19 @@ module "appsync" {
153
237
  subscription_schema = local.subscription_schema
154
238
  }
155
239
 
240
+ module "computer_runtime" {
241
+ source = "../app/computer-runtime"
242
+
243
+ stage = var.stage
244
+ account_id = var.account_id
245
+ region = var.region
246
+ vpc_id = module.vpc.vpc_id
247
+ subnet_ids = module.vpc.private_subnet_ids
248
+ task_subnet_ids = local.computer_task_subnet_ids
249
+ assign_public_ip = length(module.vpc.public_subnet_ids) > 0
250
+ appsync_api_arn = module.appsync.graphql_api_arn
251
+ }
252
+
156
253
  module "api" {
157
254
  source = "../app/lambda-api"
158
255
 
@@ -168,33 +265,121 @@ module "api" {
168
265
  graphql_db_secret_arn = module.database.graphql_db_secret_arn
169
266
  database_name = var.database_name
170
267
 
268
+ # Phase 3 U4 — compliance-outbox-drainer connects as `compliance_drainer`
269
+ # via this dedicated secret (provisioned in U2 / PR #887, populated by
270
+ # the compliance-bootstrap CI step in deploy.yml).
271
+ compliance_drainer_secret_arn = module.database.compliance_drainer_secret_arn
272
+
273
+ # Phase 3 U7 — anchor bucket + IAM role wiring. U8a now uses these to
274
+ # provision the standalone anchor Lambda function + watchdog + schedules.
275
+ compliance_anchor_bucket_arn = module.compliance_anchors.bucket_arn
276
+ compliance_anchor_bucket_name = module.compliance_anchors.bucket_name
277
+ compliance_anchor_lambda_role_arn = module.compliance_anchors.lambda_role_arn
278
+ compliance_anchor_lambda_role_name = module.compliance_anchors.lambda_role_name
279
+
280
+ # Phase 3 U8b — sibling watchdog role (kms:DescribeKey only on the CMK,
281
+ # s3:ListBucket prefix-conditioned). The watchdog moves OFF the shared
282
+ # lambda role onto this dedicated role; the move is a `terraform state
283
+ # mv` operator step documented in the U8b plan.
284
+ compliance_anchor_watchdog_role_arn = module.compliance_anchors.watchdog_role_arn
285
+ compliance_anchor_watchdog_role_name = module.compliance_anchors.watchdog_role_name
286
+
287
+ # Phase 3 U8a — anchor Lambda runtime config. compliance_reader for
288
+ # least-privilege SELECT on audit_events; retention_days forwarded as
289
+ # the COMPLIANCE_ANCHOR_RETENTION_DAYS env var (consumed by U8b's
290
+ # live function; pre-plumbed in U8a per Decision #11).
291
+ compliance_reader_secret_arn = module.database.compliance_reader_secret_arn
292
+ compliance_anchor_object_lock_retention_days = var.compliance_anchor_retention_days
293
+
294
+ # Phase 3 U8b — KMS key + Object Lock mode forwarded as
295
+ # COMPLIANCE_ANCHOR_KMS_KEY_ARN and COMPLIANCE_ANCHOR_OBJECT_LOCK_MODE
296
+ # env vars on the anchor Lambda. The live `_anchor_fn_live` requires
297
+ # both: KMS for SSE-KMS PutObject, mode for the per-object retention
298
+ # override applied to anchors/.
299
+ compliance_anchor_kms_key_arn = module.compliance_anchors.kms_key_arn
300
+ compliance_anchor_object_lock_mode = module.compliance_anchors.object_lock_mode
301
+
302
+ # Phase 3 U11.U2 — exports bucket + runner role wiring. The U11.U1
303
+ # createComplianceExport mutation dispatches jobIds to the SQS queue
304
+ # provisioned inside lambda-api; the runner Lambda assumes the role
305
+ # below and writes CSV/NDJSON artifacts to the bucket.
306
+ compliance_exports_bucket_name = module.compliance_exports.bucket_name
307
+ compliance_exports_runner_role_arn = module.compliance_exports.runner_role_arn
308
+ compliance_exports_runner_role_name = module.compliance_exports.runner_role_name
309
+
171
310
  bucket_name = module.s3.bucket_name
172
311
  bucket_arn = module.s3.bucket_arn
173
312
 
174
- user_pool_id = module.cognito.user_pool_id
175
- user_pool_arn = module.cognito.user_pool_arn
176
- admin_client_id = module.cognito.admin_client_id
177
- mobile_client_id = module.cognito.mobile_client_id
313
+ user_pool_id = module.cognito.user_pool_id
314
+ user_pool_arn = module.cognito.user_pool_arn
315
+ admin_client_id = module.cognito.admin_client_id
316
+ mobile_client_id = module.cognito.mobile_client_id
317
+ cognito_auth_domain = module.cognito.auth_domain
178
318
 
179
319
  appsync_api_url = module.appsync.graphql_api_url
180
320
  appsync_api_key = module.appsync.graphql_api_key
181
321
 
182
322
  kb_service_role_arn = module.bedrock_kb.kb_service_role_arn
183
323
 
184
- lambda_zips_dir = var.lambda_zips_dir
185
- api_auth_secret = var.api_auth_secret
186
- db_password = var.db_password
187
- agentcore_function_name = module.agentcore.agentcore_function_name
188
- agentcore_function_arn = module.agentcore.agentcore_function_arn
189
- hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
190
- agentcore_memory_id = module.agentcore_memory.memory_id
191
- memory_engine = local.resolved_memory_engine
192
- admin_url = "https://${module.admin_site.distribution_domain}"
193
- docs_url = "https://${module.docs_site.distribution_domain}"
194
- appsync_realtime_url = module.appsync.graphql_realtime_url
195
- ecr_repository_url = module.agentcore.ecr_repository_url
196
- job_scheduler_role_arn = module.job_triggers.job_scheduler_role_arn
197
- lastmile_tasks_api_url = var.lastmile_tasks_api_url
324
+ lambda_zips_dir = var.lambda_zips_dir
325
+ api_auth_secret = var.api_auth_secret
326
+ db_password = var.db_password
327
+ agentcore_function_name = module.agentcore.agentcore_function_name
328
+ agentcore_flue_function_name = module.agentcore_flue.agentcore_flue_function_name
329
+ agentcore_function_arn = module.agentcore.agentcore_function_arn
330
+ agentcore_flue_function_arn = module.agentcore_flue.agentcore_flue_function_arn
331
+ hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
332
+ agentcore_memory_id = module.agentcore_memory.memory_id
333
+ memory_engine = local.resolved_memory_engine
334
+ admin_url = var.admin_domain != "" ? "https://${var.admin_domain}" : "https://${module.admin_site.distribution_domain}"
335
+ docs_url = "https://${module.docs_site.distribution_domain}"
336
+ www_url = var.www_domain != "" ? "https://${var.www_domain}" : "https://${module.www_site.distribution_domain}"
337
+ stripe_price_ids_json = var.stripe_price_ids_json
338
+ appsync_realtime_url = module.appsync.graphql_realtime_url
339
+ ecr_repository_url = module.agentcore.ecr_repository_url
340
+ job_scheduler_role_arn = module.job_triggers.job_scheduler_role_arn
341
+ routines_execution_role_arn = module.routines_stepfunctions.execution_role_arn
342
+ routines_log_group_arn = module.routines_stepfunctions.log_group_arn
343
+ agentcore_code_interpreter_id = var.agentcore_code_interpreter_id
344
+ wiki_compile_model_id = var.wiki_compile_model_id
345
+ company_brain_source_agent_model_id = var.company_brain_source_agent_model_id
346
+ wiki_aggregation_pass_enabled = var.wiki_aggregation_pass_enabled
347
+ wiki_deterministic_linking_enabled = var.wiki_deterministic_linking_enabled
348
+ google_places_api_key = var.google_places_api_key
349
+ enable_workspace_orchestration = var.enable_workspace_orchestration
350
+ computer_runtime_cluster_name = module.computer_runtime.cluster_name
351
+ computer_runtime_cluster_arn = module.computer_runtime.cluster_arn
352
+ computer_runtime_efs_file_system_id = module.computer_runtime.efs_file_system_id
353
+ computer_runtime_subnet_ids = module.computer_runtime.task_subnet_ids
354
+ computer_runtime_assign_public_ip = module.computer_runtime.assign_public_ip
355
+ computer_runtime_task_sg_id = module.computer_runtime.task_security_group_id
356
+ computer_runtime_execution_role_arn = module.computer_runtime.execution_role_arn
357
+ computer_runtime_task_role_arn = module.computer_runtime.task_role_arn
358
+ computer_runtime_log_group_name = module.computer_runtime.log_group_name
359
+ computer_runtime_repository_url = module.computer_runtime.repository_url
360
+ computer_runtime_default_cpu = module.computer_runtime.default_cpu
361
+ computer_runtime_default_memory = module.computer_runtime.default_memory
362
+ computer_runtime_manager_policy_arn = module.computer_runtime.manager_policy_arn
363
+
364
+ # workspace-files-efs sidecar: VPC-attached Lambda that reads any Computer's
365
+ # workspace files directly off the shared EFS (bypasses the
366
+ # computer_tasks queue for list/get). See plan
367
+ # docs/plans/2026-05-13-XXX-feat-admin-computer-efs-listing-plan.md.
368
+ workspace_admin_efs_access_point_arn = module.computer_runtime.workspace_admin_access_point_arn
369
+ workspace_admin_lambda_sg_id = module.computer_runtime.workspace_admin_lambda_sg_id
370
+
371
+ # Per-user OAuth client credentials — fed to Secrets Manager in
372
+ # app/lambda-api/oauth-secrets.tf. Reuses the same google_oauth_client_*
373
+ # tfvars that already flow to the Cognito federated-signin module.
374
+ google_oauth_client_id = var.google_oauth_client_id
375
+ google_oauth_client_secret = var.google_oauth_client_secret
376
+ redirect_success_url = var.redirect_success_url
377
+ platform_operator_emails = var.platform_operator_emails
378
+
379
+ mcp_custom_domain = var.mcp_custom_domain
380
+ mcp_custom_domain_ready = var.mcp_custom_domain_ready
381
+
382
+ depends_on = [module.cognito]
198
383
  }
199
384
 
200
385
  ################################################################################
@@ -224,6 +409,99 @@ module "agentcore" {
224
409
  hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
225
410
  agentcore_memory_id = module.agentcore_memory.memory_id
226
411
  memory_engine = local.resolved_memory_engine
412
+
413
+ # Threaded through so the container's run_skill_dispatch can POST
414
+ # terminal state back to /api/skills/complete. The lambda-api module
415
+ # is declared above at line 156 as `module "api"`, so the reference
416
+ # is `module.api` — not `module.lambda_api` (which doesn't exist and
417
+ # broke terraform apply on every merge since #389).
418
+ api_endpoint = module.api.api_endpoint
419
+ api_auth_secret = var.api_auth_secret
420
+ nova_act_api_key = var.nova_act_api_key
421
+ }
422
+
423
+ ################################################################################
424
+ # AgentCore Flue — Plan §005 U2 splits the Flue Lambda + log group + IAM role
425
+ # + event-invoke config out of the Strands `agentcore-runtime` module into a
426
+ # dedicated module so Flue can carry its own permissions surface independently.
427
+ # The shared ECR repo and async DLQ stay with `module.agentcore` and are
428
+ # injected here.
429
+ ################################################################################
430
+
431
+ module "agentcore_flue" {
432
+ source = "../app/agentcore-flue"
433
+
434
+ stage = var.stage
435
+ account_id = var.account_id
436
+ region = var.region
437
+ bucket_name = module.s3.bucket_name
438
+
439
+ ecr_repository_url = module.agentcore.ecr_repository_url
440
+ async_dlq_arn = module.agentcore.agentcore_async_dlq_arn
441
+
442
+ hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
443
+ agentcore_memory_id = module.agentcore_memory.memory_id
444
+ memory_engine = local.resolved_memory_engine
445
+
446
+ api_endpoint = module.api.api_endpoint
447
+ api_auth_secret = var.api_auth_secret
448
+
449
+ # Plan §005 U4 — AuroraSessionStore uses the RDS Data API. Cluster ARN
450
+ # + secret come from the existing aurora-postgres module so Flue and
451
+ # graphql-http hit the same cluster + same credential rotation surface.
452
+ db_cluster_arn = module.database.db_cluster_arn
453
+ db_secret_arn = module.database.graphql_db_secret_arn
454
+ }
455
+
456
+ # Plan §005 U2 — cross-module state migration. The Flue resources moved from
457
+ # `module.agentcore` to `module.agentcore_flue`; the underlying AWS resource
458
+ # attributes (function_name, log group name, ARN) are unchanged from U1, so
459
+ # this is pure state-address realignment without destroy+create.
460
+ #
461
+ # Two `moved {}` blocks per resource form a CHAIN that covers both possible
462
+ # starting states:
463
+ # * Stages that never applied U1 (operator-managed greenfield, or any
464
+ # stage that skipped the U1 deploy) have state at
465
+ # `module.agentcore.aws_*.agentcore_pi` — the first block migrates that
466
+ # to `module.agentcore.aws_*.agentcore_flue` (U1's destination), then
467
+ # the second block migrates THAT to the new module.
468
+ # * Stages that applied U1 (e.g. dev) have state at
469
+ # `module.agentcore.aws_*.agentcore_flue` — only the second block
470
+ # fires.
471
+ #
472
+ # Terraform follows the chain transitively. The earlier shape (both
473
+ # blocks pointing directly at `module.agentcore_flue.…`) was rejected
474
+ # with "Ambiguous move statements" because each destination can only have
475
+ # one source — chaining through the intermediate disambiguates while still
476
+ # covering both starting states.
477
+ moved {
478
+ from = module.agentcore.aws_cloudwatch_log_group.agentcore_pi
479
+ to = module.agentcore.aws_cloudwatch_log_group.agentcore_flue
480
+ }
481
+
482
+ moved {
483
+ from = module.agentcore.aws_cloudwatch_log_group.agentcore_flue
484
+ to = module.agentcore_flue.aws_cloudwatch_log_group.agentcore_flue
485
+ }
486
+
487
+ moved {
488
+ from = module.agentcore.aws_lambda_function.agentcore_pi
489
+ to = module.agentcore.aws_lambda_function.agentcore_flue
490
+ }
491
+
492
+ moved {
493
+ from = module.agentcore.aws_lambda_function.agentcore_flue
494
+ to = module.agentcore_flue.aws_lambda_function.agentcore_flue
495
+ }
496
+
497
+ moved {
498
+ from = module.agentcore.aws_lambda_function_event_invoke_config.agentcore_pi
499
+ to = module.agentcore.aws_lambda_function_event_invoke_config.agentcore_flue
500
+ }
501
+
502
+ moved {
503
+ from = module.agentcore.aws_lambda_function_event_invoke_config.agentcore_flue
504
+ to = module.agentcore_flue.aws_lambda_function_event_invoke_config.agentcore_flue
227
505
  }
228
506
 
229
507
  module "crons" {
@@ -242,6 +520,22 @@ module "job_triggers" {
242
520
  region = var.region
243
521
  }
244
522
 
523
+ module "routines_stepfunctions" {
524
+ source = "../app/routines-stepfunctions"
525
+
526
+ stage = var.stage
527
+ account_id = var.account_id
528
+ region = var.region
529
+
530
+ # Phase B U9: EventBridge → routine-execution-callback. Constructed
531
+ # from the lambda-api naming convention rather than referencing the
532
+ # module output directly to avoid a cycle (lambda-api consumes
533
+ # routines_execution_role_arn from this module). The function exists
534
+ # for_each-iterated under aws_lambda_function.handler[*] in lambda-api;
535
+ # the ARN follows the deterministic naming pattern.
536
+ execution_callback_lambda_arn = "arn:aws:lambda:${var.region}:${var.account_id}:function:thinkwork-${var.stage}-api-routine-execution-callback"
537
+ }
538
+
245
539
  module "hindsight" {
246
540
  count = local.hindsight_enabled ? 1 : 0
247
541
  source = "../app/hindsight-memory"
@@ -283,6 +577,130 @@ module "admin_site" {
283
577
  certificate_arn = var.admin_certificate_arn
284
578
  }
285
579
 
580
+ ################################################################################
581
+ # Computer Static Site (apps/computer — end-user surface at computer.thinkwork.ai)
582
+ ################################################################################
583
+
584
+ locals {
585
+ # Host CSP for the Computer SPA (plan-012 U10 / contract v1 §CSP profile).
586
+ # Generated apps always execute in the sandbox iframe shell, so the parent
587
+ # origin never needs blob: script/worker execution for transformed modules.
588
+ computer_host_script_src = "'self'"
589
+ computer_host_worker_src = "'self'"
590
+ computer_host_frame_src = local.computer_sandbox_enabled ? "https://${var.computer_sandbox_domain}" : "'none'"
591
+ computer_host_frame_ancestors = join(" ", compact([
592
+ "'self'",
593
+ var.admin_domain != "" ? "https://${var.admin_domain}" : "",
594
+ "https://${module.admin_site.distribution_domain}",
595
+ ]))
596
+
597
+ computer_host_csp = "default-src 'self'; script-src ${local.computer_host_script_src}; style-src 'self' 'unsafe-inline'; worker-src ${local.computer_host_worker_src}; frame-src ${local.computer_host_frame_src}; connect-src 'self' https://*.execute-api.${var.region}.amazonaws.com https://*.appsync-api.${var.region}.amazonaws.com wss://*.appsync-realtime-api.${var.region}.amazonaws.com https://cognito-idp.${var.region}.amazonaws.com https://*.auth.${var.region}.amazoncognito.com; img-src 'self' data: blob: ${local.computer_sandbox_map_img_src}; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors ${local.computer_host_frame_ancestors};"
598
+ }
599
+
600
+ module "computer_site" {
601
+ source = "../app/static-site"
602
+
603
+ stage = var.stage
604
+ site_name = "computer"
605
+ is_spa = true
606
+ custom_domain = var.computer_domain
607
+ certificate_arn = var.computer_certificate_arn
608
+
609
+ # Plan-012 U10: host CSP defends the parent origin. Iframe-shell's
610
+ # own CSP (set on computer_sandbox_site below) carries the
611
+ # `connect-src 'none'` + `frame-ancestors` allowlist defense as
612
+ # belt-and-suspenders.
613
+ inline_response_headers = {
614
+ content_security_policy = local.computer_host_csp
615
+ content_type_options_override = true
616
+ strict_transport_security = {
617
+ max_age_sec = 63072000
618
+ include_subdomains = true
619
+ preload = true
620
+ override = true
621
+ }
622
+ }
623
+ }
624
+
625
+ ################################################################################
626
+ # Computer Sandbox Static Site (sandbox.thinkwork.ai — LLM-fragment iframe host)
627
+ #
628
+ # Plan-012 U3. Cross-origin sandbox subdomain that hosts the iframe-shell
629
+ # bundle. The iframe document is loaded from this distribution by the
630
+ # Computer SPA (computer_site above) via `<iframe sandbox="allow-scripts"
631
+ # src="https://sandbox.thinkwork.ai/iframe-shell.html">`. Because the
632
+ # sandbox attribute omits `allow-same-origin`, the iframe runs at an opaque
633
+ # origin — the parent uses `targetOrigin: "*"` for postMessage delivery
634
+ # and trust comes from pinned src + iframe-side parent-origin allowlist +
635
+ # channelId nonce + no-secrets-in-payload (see contract v1).
636
+ #
637
+ # Bucket is empty in this PR. U9 populates it with the iframe-shell bundle
638
+ # via scripts/build-computer.sh.
639
+ #
640
+ # Iframe CSP profile (per contract v1 §CSP profile):
641
+ # default-src 'none'; script-src 'self' blob:; worker-src 'self' blob:;
642
+ # style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:
643
+ # https://*.tile.openstreetmap.org https://api.mapbox.com;
644
+ # font-src 'self' data:; connect-src 'none';
645
+ # frame-src https://www.openstreetmap.org; object-src 'none';
646
+ # base-uri 'self'; frame-ancestors <var.computer_sandbox_allowed_parent_origins>;
647
+ #
648
+ # Provisioning is gated on var.computer_sandbox_domain — leave empty in
649
+ # stages that haven't allocated the subdomain yet.
650
+ ################################################################################
651
+
652
+ locals {
653
+ computer_sandbox_enabled = var.computer_sandbox_domain != ""
654
+
655
+ computer_sandbox_frame_ancestors = local.computer_sandbox_enabled && var.computer_sandbox_allowed_parent_origins != "" ? join(" ", split(",", replace(var.computer_sandbox_allowed_parent_origins, " ", ""))) : "'none'"
656
+
657
+ computer_sandbox_map_img_src = "https://*.tile.openstreetmap.org https://api.mapbox.com"
658
+ computer_sandbox_map_frame_src = "https://www.openstreetmap.org"
659
+
660
+ computer_sandbox_csp = "default-src 'none'; script-src 'self' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: ${local.computer_sandbox_map_img_src}; font-src 'self' data:; connect-src 'none'; frame-src ${local.computer_sandbox_map_frame_src}; object-src 'none'; base-uri 'self'; frame-ancestors ${local.computer_sandbox_frame_ancestors};"
661
+ }
662
+
663
+ module "computer_sandbox_site" {
664
+ source = "../app/static-site"
665
+ count = local.computer_sandbox_enabled ? 1 : 0
666
+
667
+ stage = var.stage
668
+ site_name = "computer-sandbox"
669
+ is_spa = false
670
+ custom_domain = var.computer_sandbox_domain
671
+ certificate_arn = var.computer_sandbox_certificate_arn
672
+
673
+ # Iframe CSP — load-bearing for the cross-origin sandbox security
674
+ # boundary. connect-src 'none' is the defense-in-depth invariant: even
675
+ # if the host CSP regresses, the iframe cannot exfiltrate via fetch /
676
+ # XHR / WebSocket because the browser blocks the request inside the
677
+ # iframe scope.
678
+ inline_response_headers = {
679
+ content_security_policy = local.computer_sandbox_csp
680
+ content_type_options_override = true
681
+ # The iframe document runs with an opaque "null" origin because the
682
+ # parent sets sandbox="allow-scripts" without allow-same-origin.
683
+ # Module-script and asset requests from that document are therefore
684
+ # CORS requests back to this distribution. Allow public reads from
685
+ # any origin; credentials are false and the sandbox CSP still keeps
686
+ # connect-src 'none'.
687
+ cors = {
688
+ allow_origins = ["*"]
689
+ allow_methods = ["GET", "HEAD", "OPTIONS"]
690
+ allow_headers = ["*"]
691
+ allow_credentials = false
692
+ max_age_sec = 600
693
+ origin_override = true
694
+ }
695
+ strict_transport_security = {
696
+ max_age_sec = 63072000 # 2 years
697
+ include_subdomains = true
698
+ preload = true
699
+ override = true
700
+ }
701
+ }
702
+ }
703
+
286
704
  ################################################################################
287
705
  # Docs Static Site
288
706
  ################################################################################