thinkwork-cli 0.12.1 → 0.12.3

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 (29) hide show
  1. package/dist/cli.js +1062 -45
  2. package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +232 -0
  3. package/dist/commands/enterprise/templates/deploy-repo/README.md +31 -0
  4. package/dist/commands/enterprise/templates/deploy-repo/customer/branding/README.md +7 -0
  5. package/dist/commands/enterprise/templates/deploy-repo/customer/deployment.json +6 -0
  6. package/dist/commands/enterprise/templates/deploy-repo/customer/evals/README.md +10 -0
  7. package/dist/commands/enterprise/templates/deploy-repo/customer/seeds/README.md +7 -0
  8. package/dist/commands/enterprise/templates/deploy-repo/customer/skills/README.md +7 -0
  9. package/dist/commands/enterprise/templates/deploy-repo/customer/workspace-defaults/README.md +7 -0
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +606 -0
  11. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +99 -0
  12. package/dist/commands/enterprise/templates/deploy-repo/terraform/backend-dev.hcl +6 -0
  13. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +101 -0
  14. package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/dev.tfvars +9 -0
  15. package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/prod.tfvars +9 -0
  16. package/dist/commands/enterprise/templates/deploy-repo/thinkwork.lock +17 -0
  17. package/dist/terraform/examples/greenfield/main.tf +26 -0
  18. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -0
  19. package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +7 -7
  20. package/dist/terraform/modules/app/lambda-api/handlers.tf +78 -68
  21. package/dist/terraform/modules/app/lambda-api/outputs.tf +9 -4
  22. package/dist/terraform/modules/app/lambda-api/remote-artifacts.tf +36 -0
  23. package/dist/terraform/modules/app/lambda-api/variables.tf +7 -0
  24. package/dist/terraform/modules/app/lambda-api/workspace-events.tf +1 -1
  25. package/dist/terraform/modules/thinkwork/main.tf +3 -2
  26. package/dist/terraform/modules/thinkwork/outputs.tf +5 -0
  27. package/dist/terraform/modules/thinkwork/variables.tf +6 -0
  28. package/dist/terraform/schema.graphql +10 -40
  29. package/package.json +1 -1
@@ -2,13 +2,13 @@
2
2
  # Real Lambda Handlers
3
3
  #
4
4
  # Each handler is bundled by scripts/build-lambdas.sh into dist/lambdas/<name>.zip.
5
- # Terraform references them via var.lambda_zips_dir (local path) for dev deploys,
6
- # or via S3 (lambda_artifact_bucket) for production.
5
+ # Terraform references them via var.lambda_zips_dir (local path) for source
6
+ # checkout deploys, or via S3 (lambda_artifact_bucket) for release deploys.
7
7
  ################################################################################
8
8
 
9
9
  locals {
10
10
  use_local_zips = var.lambda_zips_dir != ""
11
- eval_fanout_queue_url = local.use_local_zips ? aws_sqs_queue.eval_fanout[0].url : ""
11
+ eval_fanout_queue_url = local.deploy_lambda_handlers ? aws_sqs_queue.eval_fanout[0].url : ""
12
12
  runtime = "nodejs20.x"
13
13
 
14
14
  # Common environment variables shared by all API handlers
@@ -273,11 +273,11 @@ locals {
273
273
  }
274
274
 
275
275
  # ---------------------------------------------------------------------------
276
- # Helper: creates a Lambda function from a local zip
276
+ # Helper: creates Lambda functions from a local zip directory or release S3 keys
277
277
  # ---------------------------------------------------------------------------
278
278
 
279
279
  resource "aws_lambda_function" "handler" {
280
- for_each = local.use_local_zips ? toset([
280
+ for_each = local.deploy_lambda_handlers ? toset([
281
281
  "graphql-http",
282
282
  "chat-agent-invoke",
283
283
  "wakeup-processor",
@@ -503,8 +503,10 @@ resource "aws_lambda_function" "handler" {
503
503
  timeout = each.key == "wakeup-processor" ? 300 : each.key == "chat-agent-invoke" ? 300 : each.key == "workspace-event-dispatcher" ? 60 : each.key == "eval-runner" ? 900 : each.key == "eval-worker" ? 240 : each.key == "wiki-compile" ? 480 : each.key == "ontology-scan" ? 300 : each.key == "ontology-reprocess" ? 300 : each.key == "wiki-lint" ? 300 : each.key == "wiki-export" ? 600 : each.key == "wiki-bootstrap-import" ? 900 : each.key == "folder-bundle-import" ? 300 : each.key == "routine-task-python" ? 360 : 30
504
504
  memory_size = each.key == "graphql-http" ? 512 : each.key == "wakeup-processor" ? 512 : each.key == "workspace-event-dispatcher" ? 512 : each.key == "eval-runner" ? 512 : each.key == "eval-worker" ? 512 : each.key == "wiki-compile" ? 1024 : each.key == "ontology-scan" ? 512 : each.key == "wiki-export" ? 1024 : each.key == "wiki-bootstrap-import" ? 1024 : each.key == "folder-bundle-import" ? 1024 : 256
505
505
 
506
- filename = "${var.lambda_zips_dir}/${each.key}.zip"
507
- source_code_hash = filebase64sha256("${var.lambda_zips_dir}/${each.key}.zip")
506
+ filename = local.use_local_zips ? "${var.lambda_zips_dir}/${each.key}.zip" : null
507
+ source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/${each.key}.zip") : null
508
+ s3_bucket = local.use_remote_lambda_artifacts ? var.lambda_artifact_bucket : null
509
+ s3_key = local.use_remote_lambda_artifacts ? "${local.lambda_artifact_prefix}/${each.key}.zip" : null
508
510
 
509
511
  # Per-handler reserved concurrency. compliance-outbox-drainer is a
510
512
  # single-writer (per-tenant hash chain integrity depends on it — two
@@ -544,7 +546,7 @@ resource "aws_lambda_function" "handler" {
544
546
  # infrastructure-level belt-and-suspenders.
545
547
 
546
548
  resource "aws_sqs_queue" "wiki_compile_dlq" {
547
- count = local.use_local_zips ? 1 : 0
549
+ count = local.deploy_lambda_handlers ? 1 : 0
548
550
  name = "thinkwork-${var.stage}-wiki-compile-dlq"
549
551
  message_retention_seconds = 1209600 # 14 days
550
552
 
@@ -554,7 +556,7 @@ resource "aws_sqs_queue" "wiki_compile_dlq" {
554
556
  }
555
557
 
556
558
  resource "aws_iam_role_policy" "wiki_compile_dlq_send" {
557
- count = local.use_local_zips ? 1 : 0
559
+ count = local.deploy_lambda_handlers ? 1 : 0
558
560
  name = "thinkwork-${var.stage}-wiki-compile-dlq-send"
559
561
  role = aws_iam_role.lambda.id
560
562
 
@@ -569,7 +571,7 @@ resource "aws_iam_role_policy" "wiki_compile_dlq_send" {
569
571
  }
570
572
 
571
573
  resource "aws_lambda_function_event_invoke_config" "wiki_compile" {
572
- count = local.use_local_zips ? 1 : 0
574
+ count = local.deploy_lambda_handlers ? 1 : 0
573
575
  function_name = aws_lambda_function.handler["wiki-compile"].function_name
574
576
  maximum_retry_attempts = 0
575
577
  maximum_event_age_in_seconds = 3600
@@ -585,7 +587,7 @@ resource "aws_lambda_function_event_invoke_config" "wiki_compile" {
585
587
  # retries so duplicate scan invocations do not create duplicate review
586
588
  # proposals; the scan job row is the retry/observability surface.
587
589
  resource "aws_sqs_queue" "ontology_scan_dlq" {
588
- count = local.use_local_zips ? 1 : 0
590
+ count = local.deploy_lambda_handlers ? 1 : 0
589
591
  name = "thinkwork-${var.stage}-ontology-scan-dlq"
590
592
  message_retention_seconds = 1209600 # 14 days
591
593
 
@@ -595,7 +597,7 @@ resource "aws_sqs_queue" "ontology_scan_dlq" {
595
597
  }
596
598
 
597
599
  resource "aws_iam_role_policy" "ontology_scan_dlq_send" {
598
- count = local.use_local_zips ? 1 : 0
600
+ count = local.deploy_lambda_handlers ? 1 : 0
599
601
  name = "thinkwork-${var.stage}-ontology-scan-dlq-send"
600
602
  role = aws_iam_role.lambda.id
601
603
 
@@ -610,7 +612,7 @@ resource "aws_iam_role_policy" "ontology_scan_dlq_send" {
610
612
  }
611
613
 
612
614
  resource "aws_lambda_function_event_invoke_config" "ontology_scan" {
613
- count = local.use_local_zips ? 1 : 0
615
+ count = local.deploy_lambda_handlers ? 1 : 0
614
616
  function_name = aws_lambda_function.handler["ontology-scan"].function_name
615
617
  maximum_retry_attempts = 0
616
618
  maximum_event_age_in_seconds = 3600
@@ -625,7 +627,7 @@ resource "aws_lambda_function_event_invoke_config" "ontology_scan" {
625
627
  # Ontology reprocess jobs are row-ledger driven and explicitly claim work.
626
628
  # Disable AWS async retries to keep failure/retry state in ontology.reprocess_jobs.
627
629
  resource "aws_sqs_queue" "ontology_reprocess_dlq" {
628
- count = local.use_local_zips ? 1 : 0
630
+ count = local.deploy_lambda_handlers ? 1 : 0
629
631
  name = "thinkwork-${var.stage}-ontology-reprocess-dlq"
630
632
  message_retention_seconds = 1209600 # 14 days
631
633
 
@@ -635,7 +637,7 @@ resource "aws_sqs_queue" "ontology_reprocess_dlq" {
635
637
  }
636
638
 
637
639
  resource "aws_iam_role_policy" "ontology_reprocess_dlq_send" {
638
- count = local.use_local_zips ? 1 : 0
640
+ count = local.deploy_lambda_handlers ? 1 : 0
639
641
  name = "thinkwork-${var.stage}-ontology-reprocess-dlq-send"
640
642
  role = aws_iam_role.lambda.id
641
643
 
@@ -650,7 +652,7 @@ resource "aws_iam_role_policy" "ontology_reprocess_dlq_send" {
650
652
  }
651
653
 
652
654
  resource "aws_lambda_function_event_invoke_config" "ontology_reprocess" {
653
- count = local.use_local_zips ? 1 : 0
655
+ count = local.deploy_lambda_handlers ? 1 : 0
654
656
  function_name = aws_lambda_function.handler["ontology-reprocess"].function_name
655
657
  maximum_retry_attempts = 0
656
658
  maximum_event_age_in_seconds = 3600
@@ -671,7 +673,7 @@ resource "aws_lambda_function_event_invoke_config" "ontology_reprocess" {
671
673
  # failures. SFN is the canonical retry path; Lambda async retries are
672
674
  # off. Per project_async_retry_idempotency_lessons.
673
675
  resource "aws_lambda_function_event_invoke_config" "routine_approval_callback" {
674
- count = local.use_local_zips ? 1 : 0
676
+ count = local.deploy_lambda_handlers ? 1 : 0
675
677
  function_name = aws_lambda_function.handler["routine-approval-callback"].function_name
676
678
  maximum_retry_attempts = 0
677
679
  maximum_event_age_in_seconds = 3600
@@ -687,7 +689,7 @@ resource "aws_lambda_function_event_invoke_config" "routine_approval_callback" {
687
689
  # is NOT idempotent — retries multiply LLM cost. Per
688
690
  # project_async_retry_idempotency_lessons.
689
691
  resource "aws_lambda_function_event_invoke_config" "memory_retain" {
690
- count = local.use_local_zips ? 1 : 0
692
+ count = local.deploy_lambda_handlers ? 1 : 0
691
693
  function_name = aws_lambda_function.handler["memory-retain"].function_name
692
694
  maximum_retry_attempts = 0
693
695
  maximum_event_age_in_seconds = 3600
@@ -704,7 +706,7 @@ resource "aws_lambda_function_event_invoke_config" "memory_retain" {
704
706
  # ---------------------------------------------------------------------------
705
707
 
706
708
  resource "aws_sqs_queue" "compliance_drainer_dlq" {
707
- count = local.use_local_zips ? 1 : 0
709
+ count = local.deploy_lambda_handlers ? 1 : 0
708
710
  name = "thinkwork-${var.stage}-compliance-drainer-dlq"
709
711
  message_retention_seconds = 1209600 # 14 days
710
712
 
@@ -714,7 +716,7 @@ resource "aws_sqs_queue" "compliance_drainer_dlq" {
714
716
  }
715
717
 
716
718
  resource "aws_iam_role_policy" "compliance_drainer_dlq_send" {
717
- count = local.use_local_zips ? 1 : 0
719
+ count = local.deploy_lambda_handlers ? 1 : 0
718
720
  name = "compliance-drainer-dlq-send"
719
721
  role = aws_iam_role.lambda.id
720
722
 
@@ -729,7 +731,7 @@ resource "aws_iam_role_policy" "compliance_drainer_dlq_send" {
729
731
  }
730
732
 
731
733
  resource "aws_lambda_function_event_invoke_config" "compliance_outbox_drainer" {
732
- count = local.use_local_zips ? 1 : 0
734
+ count = local.deploy_lambda_handlers ? 1 : 0
733
735
  function_name = aws_lambda_function.handler["compliance-outbox-drainer"].function_name
734
736
  maximum_retry_attempts = 0
735
737
  maximum_event_age_in_seconds = 3600
@@ -746,7 +748,7 @@ resource "aws_lambda_function_event_invoke_config" "compliance_outbox_drainer" {
746
748
  # ---------------------------------------------------------------------------
747
749
 
748
750
  resource "aws_scheduler_schedule" "compliance_outbox_drainer" {
749
- count = local.use_local_zips ? 1 : 0
751
+ count = local.deploy_lambda_handlers ? 1 : 0
750
752
 
751
753
  name = "thinkwork-${var.stage}-compliance-outbox-drainer"
752
754
  group_name = "default"
@@ -769,7 +771,7 @@ resource "aws_scheduler_schedule" "compliance_outbox_drainer" {
769
771
 
770
772
  locals {
771
773
  # Map of route_key → handler name for API Gateway
772
- api_routes = local.use_local_zips ? {
774
+ api_routes = local.deploy_lambda_handlers ? {
773
775
  # GraphQL — the main API entry point
774
776
  "POST /graphql" = "graphql-http"
775
777
  "GET /graphql" = "graphql-http"
@@ -1065,7 +1067,7 @@ resource "aws_apigatewayv2_route" "handler" {
1065
1067
  }
1066
1068
 
1067
1069
  resource "aws_lambda_permission" "handler_apigw" {
1068
- for_each = local.use_local_zips ? toset(distinct(values(local.api_routes))) : toset([])
1070
+ for_each = local.deploy_lambda_handlers ? toset(distinct(values(local.api_routes))) : toset([])
1069
1071
 
1070
1072
  statement_id = "AllowAPIGateway"
1071
1073
  action = "lambda:InvokeFunction"
@@ -1079,7 +1081,7 @@ resource "aws_lambda_permission" "handler_apigw" {
1079
1081
  # ---------------------------------------------------------------------------
1080
1082
 
1081
1083
  resource "aws_scheduler_schedule" "wakeup_processor" {
1082
- count = local.use_local_zips ? 1 : 0
1084
+ count = local.deploy_lambda_handlers ? 1 : 0
1083
1085
 
1084
1086
  name = "thinkwork-${var.stage}-wakeup-processor"
1085
1087
  group_name = "default"
@@ -1101,7 +1103,7 @@ resource "aws_scheduler_schedule" "wakeup_processor" {
1101
1103
  # ---------------------------------------------------------------------------
1102
1104
 
1103
1105
  resource "aws_scheduler_schedule" "webhook_deliveries_cleanup" {
1104
- count = local.use_local_zips ? 1 : 0
1106
+ count = local.deploy_lambda_handlers ? 1 : 0
1105
1107
 
1106
1108
  name = "thinkwork-${var.stage}-webhook-deliveries-cleanup"
1107
1109
  group_name = "default"
@@ -1126,7 +1128,7 @@ resource "aws_scheduler_schedule" "webhook_deliveries_cleanup" {
1126
1128
  # ---------------------------------------------------------------------------
1127
1129
 
1128
1130
  resource "aws_scheduler_schedule" "plugin_staging_sweeper" {
1129
- count = local.use_local_zips ? 1 : 0
1131
+ count = local.deploy_lambda_handlers ? 1 : 0
1130
1132
 
1131
1133
  name = "thinkwork-${var.stage}-plugin-staging-sweeper"
1132
1134
  group_name = "default"
@@ -1151,7 +1153,7 @@ resource "aws_scheduler_schedule" "plugin_staging_sweeper" {
1151
1153
  # ---------------------------------------------------------------------------
1152
1154
 
1153
1155
  resource "aws_scheduler_schedule" "mcp_approval_sweeper" {
1154
- count = local.use_local_zips ? 1 : 0
1156
+ count = local.deploy_lambda_handlers ? 1 : 0
1155
1157
 
1156
1158
  name = "thinkwork-${var.stage}-mcp-approval-sweeper"
1157
1159
  group_name = "default"
@@ -1177,7 +1179,7 @@ resource "aws_scheduler_schedule" "mcp_approval_sweeper" {
1177
1179
  # ---------------------------------------------------------------------------
1178
1180
 
1179
1181
  resource "aws_scheduler_schedule" "skill_runs_reconciler" {
1180
- count = local.use_local_zips ? 1 : 0
1182
+ count = local.deploy_lambda_handlers ? 1 : 0
1181
1183
 
1182
1184
  name = "thinkwork-${var.stage}-skill-runs-reconciler"
1183
1185
  group_name = "default"
@@ -1202,7 +1204,7 @@ resource "aws_scheduler_schedule" "skill_runs_reconciler" {
1202
1204
  # ---------------------------------------------------------------------------
1203
1205
 
1204
1206
  resource "aws_scheduler_schedule" "eval_runs_reconciler" {
1205
- count = local.use_local_zips ? 1 : 0
1207
+ count = local.deploy_lambda_handlers ? 1 : 0
1206
1208
 
1207
1209
  name = "thinkwork-${var.stage}-eval-runs-reconciler"
1208
1210
  group_name = "default"
@@ -1226,7 +1228,7 @@ resource "aws_scheduler_schedule" "eval_runs_reconciler" {
1226
1228
  # ---------------------------------------------------------------------------
1227
1229
 
1228
1230
  resource "aws_scheduler_schedule" "stall_monitor" {
1229
- count = local.use_local_zips ? 1 : 0
1231
+ count = local.deploy_lambda_handlers ? 1 : 0
1230
1232
 
1231
1233
  name = "thinkwork-${var.stage}-stall-monitor"
1232
1234
  group_name = "default"
@@ -1251,7 +1253,7 @@ resource "aws_scheduler_schedule" "stall_monitor" {
1251
1253
  # ---------------------------------------------------------------------------
1252
1254
 
1253
1255
  resource "aws_scheduler_schedule" "computer_runtime_reconciler" {
1254
- count = local.use_local_zips ? 1 : 0
1256
+ count = local.deploy_lambda_handlers ? 1 : 0
1255
1257
 
1256
1258
  name = "thinkwork-${var.stage}-computer-runtime-reconciler"
1257
1259
  group_name = "default"
@@ -1269,7 +1271,7 @@ resource "aws_scheduler_schedule" "computer_runtime_reconciler" {
1269
1271
  }
1270
1272
 
1271
1273
  resource "aws_scheduler_schedule" "slack_dispatch" {
1272
- count = local.use_local_zips ? 1 : 0
1274
+ count = local.deploy_lambda_handlers ? 1 : 0
1273
1275
 
1274
1276
  name = "thinkwork-${var.stage}-slack-dispatch"
1275
1277
  group_name = "default"
@@ -1292,7 +1294,7 @@ resource "aws_scheduler_schedule" "slack_dispatch" {
1292
1294
  # ---------------------------------------------------------------------------
1293
1295
 
1294
1296
  resource "aws_scheduler_schedule" "wiki_compile_drainer" {
1295
- count = local.use_local_zips ? 1 : 0
1297
+ count = local.deploy_lambda_handlers ? 1 : 0
1296
1298
 
1297
1299
  name = "thinkwork-${var.stage}-wiki-compile-drainer"
1298
1300
  group_name = "default"
@@ -1310,7 +1312,7 @@ resource "aws_scheduler_schedule" "wiki_compile_drainer" {
1310
1312
  }
1311
1313
 
1312
1314
  resource "aws_scheduler_schedule" "wiki_lint" {
1313
- count = local.use_local_zips ? 1 : 0
1315
+ count = local.deploy_lambda_handlers ? 1 : 0
1314
1316
 
1315
1317
  name = "thinkwork-${var.stage}-wiki-lint"
1316
1318
  group_name = "default"
@@ -1328,7 +1330,7 @@ resource "aws_scheduler_schedule" "wiki_lint" {
1328
1330
  }
1329
1331
 
1330
1332
  resource "aws_scheduler_schedule" "wiki_export" {
1331
- count = local.use_local_zips ? 1 : 0
1333
+ count = local.deploy_lambda_handlers ? 1 : 0
1332
1334
 
1333
1335
  name = "thinkwork-${var.stage}-wiki-export"
1334
1336
  group_name = "default"
@@ -1428,13 +1430,13 @@ resource "aws_iam_role_policy" "scheduler_invoke" {
1428
1430
  # Includes every for_each handler PLUS the standalone Phase 3 U8a
1429
1431
  # anchor Lambda (which is intentionally outside the for_each set
1430
1432
  # because it uses the U7 IAM role, not the shared aws_iam_role.lambda).
1431
- # Splat (`[*]`) expansion handles count=0 cleanly when local.use_local_zips
1432
- # is false; an indexed reference (`[0].arn`) would throw on graph eval.
1433
+ # Splat (`[*]`) expansion handles count=0 cleanly when handlers are
1434
+ # disabled; an indexed reference (`[0].arn`) would throw on graph eval.
1433
1435
  # Phase 3 U8b — watchdog moved to standalone resource; its ARN must
1434
1436
  # be added to the splat list explicitly (SEC-U8B-005). The splat
1435
- # `[*].arn` form handles count = 0 cleanly when local.use_local_zips
1436
- # is false; an indexed `[0].arn` would throw on graph eval.
1437
- Resource = local.use_local_zips ? concat(
1437
+ # `[*].arn` form handles count = 0 cleanly when handlers are disabled;
1438
+ # an indexed `[0].arn` would throw on graph eval.
1439
+ Resource = local.deploy_lambda_handlers ? concat(
1438
1440
  [for k, v in aws_lambda_function.handler : v.arn],
1439
1441
  aws_lambda_function.compliance_anchor[*].arn,
1440
1442
  aws_lambda_function.compliance_anchor_watchdog[*].arn,
@@ -1476,7 +1478,7 @@ resource "aws_ssm_parameter" "google_places_api_key" {
1476
1478
  }
1477
1479
 
1478
1480
  resource "aws_ssm_parameter" "lambda_arns" {
1479
- for_each = local.use_local_zips ? {
1481
+ for_each = local.deploy_lambda_handlers ? {
1480
1482
  "chat-agent-invoke-fn-arn" = aws_lambda_function.handler["chat-agent-invoke"].arn
1481
1483
  "kb-manager-fn-arn" = aws_lambda_function.handler["knowledge-base-manager"].arn
1482
1484
  "job-schedule-manager-fn-arn" = aws_lambda_function.handler["job-schedule-manager"].arn
@@ -1508,7 +1510,7 @@ resource "aws_ssm_parameter" "lambda_arns" {
1508
1510
  # ===========================================================================
1509
1511
 
1510
1512
  resource "aws_lambda_function" "compliance_anchor" {
1511
- count = local.use_local_zips ? 1 : 0
1513
+ count = local.deploy_lambda_handlers ? 1 : 0
1512
1514
 
1513
1515
  function_name = "thinkwork-${var.stage}-api-compliance-anchor"
1514
1516
  role = var.compliance_anchor_lambda_role_arn
@@ -1516,8 +1518,10 @@ resource "aws_lambda_function" "compliance_anchor" {
1516
1518
  runtime = local.runtime
1517
1519
  timeout = 60
1518
1520
  memory_size = 1024
1519
- filename = "${var.lambda_zips_dir}/compliance-anchor.zip"
1520
- source_code_hash = filebase64sha256("${var.lambda_zips_dir}/compliance-anchor.zip")
1521
+ filename = local.use_local_zips ? "${var.lambda_zips_dir}/compliance-anchor.zip" : null
1522
+ source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/compliance-anchor.zip") : null
1523
+ s3_bucket = local.use_remote_lambda_artifacts ? var.lambda_artifact_bucket : null
1524
+ s3_key = local.use_remote_lambda_artifacts ? "${local.lambda_artifact_prefix}/compliance-anchor.zip" : null
1521
1525
  reserved_concurrent_executions = 1
1522
1526
 
1523
1527
  environment {
@@ -1538,14 +1542,14 @@ resource "aws_lambda_function" "compliance_anchor" {
1538
1542
  }
1539
1543
 
1540
1544
  resource "aws_sqs_queue" "compliance_anchor_dlq" {
1541
- count = local.use_local_zips ? 1 : 0
1545
+ count = local.deploy_lambda_handlers ? 1 : 0
1542
1546
  name = "thinkwork-${var.stage}-compliance-anchor-dlq"
1543
1547
  message_retention_seconds = 1209600 # 14 days, matches the drainer DLQ
1544
1548
  sqs_managed_sse_enabled = true
1545
1549
  }
1546
1550
 
1547
1551
  resource "aws_iam_role_policy" "compliance_anchor_dlq_send" {
1548
- count = local.use_local_zips ? 1 : 0
1552
+ count = local.deploy_lambda_handlers ? 1 : 0
1549
1553
  name = "compliance-anchor-dlq-send"
1550
1554
  # Attached to the U7 anchor role (which the standalone anchor Lambda assumes).
1551
1555
  role = var.compliance_anchor_lambda_role_name
@@ -1561,7 +1565,7 @@ resource "aws_iam_role_policy" "compliance_anchor_dlq_send" {
1561
1565
  }
1562
1566
 
1563
1567
  resource "aws_lambda_function_event_invoke_config" "compliance_anchor" {
1564
- count = local.use_local_zips ? 1 : 0
1568
+ count = local.deploy_lambda_handlers ? 1 : 0
1565
1569
  function_name = aws_lambda_function.compliance_anchor[0].function_name
1566
1570
  maximum_retry_attempts = 0
1567
1571
  maximum_event_age_in_seconds = 3600
@@ -1589,7 +1593,7 @@ resource "aws_lambda_function_event_invoke_config" "compliance_anchor" {
1589
1593
  # ---------------------------------------------------------------------------
1590
1594
 
1591
1595
  resource "aws_lambda_function" "compliance_anchor_watchdog" {
1592
- count = local.use_local_zips ? 1 : 0
1596
+ count = local.deploy_lambda_handlers ? 1 : 0
1593
1597
 
1594
1598
  function_name = "thinkwork-${var.stage}-api-compliance-anchor-watchdog"
1595
1599
  role = var.compliance_anchor_watchdog_role_arn
@@ -1597,8 +1601,10 @@ resource "aws_lambda_function" "compliance_anchor_watchdog" {
1597
1601
  runtime = local.runtime
1598
1602
  timeout = 30
1599
1603
  memory_size = 512
1600
- filename = "${var.lambda_zips_dir}/compliance-anchor-watchdog.zip"
1601
- source_code_hash = filebase64sha256("${var.lambda_zips_dir}/compliance-anchor-watchdog.zip")
1604
+ filename = local.use_local_zips ? "${var.lambda_zips_dir}/compliance-anchor-watchdog.zip" : null
1605
+ source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/compliance-anchor-watchdog.zip") : null
1606
+ s3_bucket = local.use_remote_lambda_artifacts ? var.lambda_artifact_bucket : null
1607
+ s3_key = local.use_remote_lambda_artifacts ? "${local.lambda_artifact_prefix}/compliance-anchor-watchdog.zip" : null
1602
1608
 
1603
1609
  environment {
1604
1610
  variables = {
@@ -1620,7 +1626,7 @@ resource "aws_lambda_function" "compliance_anchor_watchdog" {
1620
1626
  # ---------------------------------------------------------------------------
1621
1627
 
1622
1628
  resource "aws_scheduler_schedule" "compliance_anchor" {
1623
- count = local.use_local_zips ? 1 : 0
1629
+ count = local.deploy_lambda_handlers ? 1 : 0
1624
1630
 
1625
1631
  name = "thinkwork-${var.stage}-compliance-anchor"
1626
1632
  group_name = "default"
@@ -1642,7 +1648,7 @@ resource "aws_scheduler_schedule" "compliance_anchor" {
1642
1648
  }
1643
1649
 
1644
1650
  resource "aws_scheduler_schedule" "compliance_anchor_watchdog" {
1645
- count = local.use_local_zips ? 1 : 0
1651
+ count = local.deploy_lambda_handlers ? 1 : 0
1646
1652
 
1647
1653
  name = "thinkwork-${var.stage}-compliance-anchor-watchdog"
1648
1654
  group_name = "default"
@@ -1685,7 +1691,7 @@ resource "aws_scheduler_schedule" "compliance_anchor_watchdog" {
1685
1691
  # ---------------------------------------------------------------------------
1686
1692
 
1687
1693
  resource "aws_cloudwatch_metric_alarm" "compliance_anchor_gap" {
1688
- count = local.use_local_zips ? 1 : 0
1694
+ count = local.deploy_lambda_handlers ? 1 : 0
1689
1695
 
1690
1696
  alarm_name = "thinkwork-${var.stage}-compliance-anchor-gap"
1691
1697
  alarm_description = "Anchor cadence gap exceeded threshold. LIVE in U8b — fires on >=1 ComplianceAnchorGap=1 OR missing metric (means watchdog broken)."
@@ -1705,7 +1711,7 @@ resource "aws_cloudwatch_metric_alarm" "compliance_anchor_gap" {
1705
1711
  }
1706
1712
 
1707
1713
  resource "aws_cloudwatch_metric_alarm" "compliance_anchor_watchdog_heartbeat_missing" {
1708
- count = local.use_local_zips ? 1 : 0
1714
+ count = local.deploy_lambda_handlers ? 1 : 0
1709
1715
 
1710
1716
  alarm_name = "thinkwork-${var.stage}-compliance-anchor-watchdog-heartbeat-missing"
1711
1717
  alarm_description = "Watchdog heartbeat metric is missing. LIVE in U8b — born with treat_missing_data = notBreaching to absorb deploy-time gaps; promote to breaching in a follow-up after first soak."
@@ -1748,7 +1754,7 @@ resource "aws_cloudwatch_metric_alarm" "compliance_anchor_watchdog_heartbeat_mis
1748
1754
  # ---------------------------------------------------------------------------
1749
1755
 
1750
1756
  resource "aws_sqs_queue" "compliance_exports_dlq" {
1751
- count = local.use_local_zips ? 1 : 0
1757
+ count = local.deploy_lambda_handlers ? 1 : 0
1752
1758
  name = "thinkwork-${var.stage}-compliance-exports-dlq"
1753
1759
  message_retention_seconds = 1209600 # 14 days
1754
1760
  sqs_managed_sse_enabled = true
@@ -1759,7 +1765,7 @@ resource "aws_sqs_queue" "compliance_exports_dlq" {
1759
1765
  }
1760
1766
 
1761
1767
  resource "aws_sqs_queue" "compliance_exports" {
1762
- count = local.use_local_zips ? 1 : 0
1768
+ count = local.deploy_lambda_handlers ? 1 : 0
1763
1769
  name = "thinkwork-${var.stage}-compliance-exports"
1764
1770
  visibility_timeout_seconds = 900 # matches Lambda 15-min timeout
1765
1771
  message_retention_seconds = 86400 # 1 day; DLQ holds longer-stuck messages
@@ -1779,7 +1785,7 @@ resource "aws_sqs_queue" "compliance_exports" {
1779
1785
  # from the createComplianceExport mutation. Attached to the shared
1780
1786
  # lambda role (which graphql-http assumes); scope is queue-specific.
1781
1787
  resource "aws_iam_role_policy" "compliance_exports_send" {
1782
- count = local.use_local_zips ? 1 : 0
1788
+ count = local.deploy_lambda_handlers ? 1 : 0
1783
1789
  name = "compliance-exports-send"
1784
1790
  role = aws_iam_role.lambda.id
1785
1791
 
@@ -1795,7 +1801,7 @@ resource "aws_iam_role_policy" "compliance_exports_send" {
1795
1801
 
1796
1802
  # Runner role's SQS receive grants — only the runner consumes the queue.
1797
1803
  resource "aws_iam_role_policy" "compliance_exports_runner_sqs" {
1798
- count = local.use_local_zips ? 1 : 0
1804
+ count = local.deploy_lambda_handlers ? 1 : 0
1799
1805
  name = "compliance-exports-runner-sqs"
1800
1806
  role = var.compliance_exports_runner_role_name
1801
1807
 
@@ -1824,7 +1830,7 @@ resource "aws_iam_role_policy" "compliance_exports_runner_sqs" {
1824
1830
  }
1825
1831
 
1826
1832
  resource "aws_lambda_function" "compliance_export_runner" {
1827
- count = local.use_local_zips ? 1 : 0
1833
+ count = local.deploy_lambda_handlers ? 1 : 0
1828
1834
 
1829
1835
  function_name = "thinkwork-${var.stage}-api-compliance-export-runner"
1830
1836
  role = var.compliance_exports_runner_role_arn
@@ -1832,8 +1838,10 @@ resource "aws_lambda_function" "compliance_export_runner" {
1832
1838
  runtime = local.runtime
1833
1839
  timeout = 900
1834
1840
  memory_size = 1024
1835
- filename = "${var.lambda_zips_dir}/compliance-export-runner.zip"
1836
- source_code_hash = filebase64sha256("${var.lambda_zips_dir}/compliance-export-runner.zip")
1841
+ filename = local.use_local_zips ? "${var.lambda_zips_dir}/compliance-export-runner.zip" : null
1842
+ source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/compliance-export-runner.zip") : null
1843
+ s3_bucket = local.use_remote_lambda_artifacts ? var.lambda_artifact_bucket : null
1844
+ s3_key = local.use_remote_lambda_artifacts ? "${local.lambda_artifact_prefix}/compliance-export-runner.zip" : null
1837
1845
  reserved_concurrent_executions = 2
1838
1846
 
1839
1847
  environment {
@@ -1866,7 +1874,7 @@ resource "aws_lambda_function" "compliance_export_runner" {
1866
1874
  # newer aws provider version than this codebase currently pins, and the
1867
1875
  # function-level reservation gives the equivalent ceiling at v1 scale.
1868
1876
  resource "aws_lambda_event_source_mapping" "compliance_exports" {
1869
- count = local.use_local_zips ? 1 : 0
1877
+ count = local.deploy_lambda_handlers ? 1 : 0
1870
1878
 
1871
1879
  event_source_arn = aws_sqs_queue.compliance_exports[0].arn
1872
1880
  function_name = aws_lambda_function.compliance_export_runner[0].function_name
@@ -1876,7 +1884,7 @@ resource "aws_lambda_event_source_mapping" "compliance_exports" {
1876
1884
  }
1877
1885
 
1878
1886
  resource "aws_cloudwatch_metric_alarm" "compliance_exports_dlq_depth" {
1879
- count = local.use_local_zips ? 1 : 0
1887
+ count = local.deploy_lambda_handlers ? 1 : 0
1880
1888
 
1881
1889
  alarm_name = "thinkwork-${var.stage}-compliance-exports-dlq-depth"
1882
1890
  alarm_description = "Compliance exports DLQ has messages — runner Lambda crashed (or is inert pre-U11.U3); operator must inspect."
@@ -1921,7 +1929,7 @@ resource "aws_cloudwatch_metric_alarm" "compliance_exports_dlq_depth" {
1921
1929
  # ---------------------------------------------------------------------------
1922
1930
 
1923
1931
  resource "aws_lambda_function" "workspace_files_efs" {
1924
- count = local.use_local_zips ? 1 : 0
1932
+ count = local.deploy_lambda_handlers ? 1 : 0
1925
1933
 
1926
1934
  function_name = "thinkwork-${var.stage}-api-workspace-files-efs"
1927
1935
  role = aws_iam_role.lambda.arn
@@ -1930,8 +1938,10 @@ resource "aws_lambda_function" "workspace_files_efs" {
1930
1938
  timeout = 30
1931
1939
  memory_size = 512
1932
1940
 
1933
- filename = "${var.lambda_zips_dir}/workspace-files-efs.zip"
1934
- source_code_hash = filebase64sha256("${var.lambda_zips_dir}/workspace-files-efs.zip")
1941
+ filename = local.use_local_zips ? "${var.lambda_zips_dir}/workspace-files-efs.zip" : null
1942
+ source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/workspace-files-efs.zip") : null
1943
+ s3_bucket = local.use_remote_lambda_artifacts ? var.lambda_artifact_bucket : null
1944
+ s3_key = local.use_remote_lambda_artifacts ? "${local.lambda_artifact_prefix}/workspace-files-efs.zip" : null
1935
1945
 
1936
1946
  vpc_config {
1937
1947
  subnet_ids = var.computer_runtime_subnet_ids
@@ -1962,7 +1972,7 @@ resource "aws_lambda_function" "workspace_files_efs" {
1962
1972
  # VPC. AWSLambdaVPCAccessExecutionRole gives Create/Describe/DeleteNetwork
1963
1973
  # Interface — minimum scope for VPC Lambdas.
1964
1974
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
1965
- count = local.use_local_zips ? 1 : 0
1975
+ count = local.deploy_lambda_handlers ? 1 : 0
1966
1976
 
1967
1977
  role = aws_iam_role.lambda.name
1968
1978
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
@@ -28,24 +28,29 @@ output "lambda_role_name" {
28
28
  value = aws_iam_role.lambda.name
29
29
  }
30
30
 
31
+ output "lambda_artifact_mode" {
32
+ description = "Resolved Lambda artifact source mode: local, s3, or placeholder."
33
+ value = local.lambda_artifact_mode
34
+ }
35
+
31
36
  output "memory_retain_fn_name" {
32
37
  description = "Memory-retain Lambda function name. Strands runtime invokes this directly to push conversational turns into the active memory engine."
33
- value = local.use_local_zips ? aws_lambda_function.handler["memory-retain"].function_name : ""
38
+ value = local.deploy_lambda_handlers ? aws_lambda_function.handler["memory-retain"].function_name : ""
34
39
  }
35
40
 
36
41
  output "memory_retain_fn_arn" {
37
42
  description = "Memory-retain Lambda ARN. Used to grant lambda:InvokeFunction to the agentcore-runtime role."
38
- value = local.use_local_zips ? aws_lambda_function.handler["memory-retain"].arn : ""
43
+ value = local.deploy_lambda_handlers ? aws_lambda_function.handler["memory-retain"].arn : ""
39
44
  }
40
45
 
41
46
  output "email_inbound_fn_arn" {
42
47
  description = "email-inbound Lambda ARN. Used by the SES module to wire the receipt rule Lambda action."
43
- value = local.use_local_zips ? aws_lambda_function.handler["email-inbound"].arn : ""
48
+ value = local.deploy_lambda_handlers ? aws_lambda_function.handler["email-inbound"].arn : ""
44
49
  }
45
50
 
46
51
  output "email_inbound_fn_name" {
47
52
  description = "email-inbound Lambda function name. Used by the SES module for lambda:InvokeFunction permissions."
48
- value = local.use_local_zips ? aws_lambda_function.handler["email-inbound"].function_name : ""
53
+ value = local.deploy_lambda_handlers ? aws_lambda_function.handler["email-inbound"].function_name : ""
49
54
  }
50
55
 
51
56
  # ---------------------------------------------------------------------------
@@ -0,0 +1,36 @@
1
+ ################################################################################
2
+ # Lambda Release Artifact Sources
3
+ #
4
+ # Source-repo deploys build zips locally and sets var.lambda_zips_dir.
5
+ # Enterprise deployment repos upload release zips to a customer-owned S3 bucket
6
+ # and set var.lambda_artifact_bucket + var.lambda_artifact_prefix.
7
+ ################################################################################
8
+
9
+ locals {
10
+ use_remote_lambda_artifacts = trimspace(var.lambda_artifact_bucket) != ""
11
+ lambda_artifact_prefix = trim(trimspace(var.lambda_artifact_prefix), "/")
12
+ lambda_artifact_source_count = (local.use_local_zips ? 1 : 0) + (local.use_remote_lambda_artifacts ? 1 : 0)
13
+ deploy_lambda_handlers = local.lambda_artifact_source_count == 1
14
+ lambda_artifact_mode = local.use_local_zips ? "local" : local.use_remote_lambda_artifacts ? "s3" : "placeholder"
15
+ }
16
+
17
+ resource "terraform_data" "lambda_artifact_validation" {
18
+ input = local.lambda_artifact_mode
19
+
20
+ lifecycle {
21
+ precondition {
22
+ condition = local.lambda_artifact_source_count <= 1
23
+ error_message = "Set only one Lambda artifact source: lambda_zips_dir for source checkouts, or lambda_artifact_bucket/lambda_artifact_prefix for release artifacts."
24
+ }
25
+
26
+ precondition {
27
+ condition = !local.use_remote_lambda_artifacts || local.lambda_artifact_prefix != ""
28
+ error_message = "lambda_artifact_prefix must be set when lambda_artifact_bucket is set."
29
+ }
30
+
31
+ precondition {
32
+ condition = !var.require_lambda_artifacts || local.deploy_lambda_handlers
33
+ error_message = "require_lambda_artifacts=true requires either lambda_zips_dir or lambda_artifact_bucket/lambda_artifact_prefix."
34
+ }
35
+ }
36
+ }
@@ -16,6 +16,7 @@ variable "region" {
16
16
  variable "lambda_artifact_bucket" {
17
17
  description = "S3 bucket containing Lambda deployment artifacts"
18
18
  type = string
19
+ default = ""
19
20
  }
20
21
 
21
22
  variable "lambda_artifact_prefix" {
@@ -126,6 +127,12 @@ variable "lambda_zips_dir" {
126
127
  default = ""
127
128
  }
128
129
 
130
+ variable "require_lambda_artifacts" {
131
+ description = "Fail planning unless either lambda_zips_dir or lambda_artifact_bucket/lambda_artifact_prefix is configured. Enterprise deployment repos should set this to true."
132
+ type = bool
133
+ default = false
134
+ }
135
+
129
136
  variable "db_password" {
130
137
  description = "Database password (used to construct DATABASE_URL for Lambda)"
131
138
  type = string
@@ -1,5 +1,5 @@
1
1
  locals {
2
- workspace_event_enabled = var.enable_workspace_orchestration && local.use_local_zips
2
+ workspace_event_enabled = var.enable_workspace_orchestration && local.deploy_lambda_handlers
3
3
  # EventBridge rejects the more precise per-folder S3 wildcard rules as too
4
4
  # complex. Keep broad workspace/catalog families in separate rules and let the
5
5
  # dispatcher keep the canonical allowlist in code.
@@ -257,8 +257,9 @@ module "api" {
257
257
  account_id = var.account_id
258
258
  region = var.region
259
259
 
260
- lambda_artifact_bucket = var.lambda_artifact_bucket
261
- lambda_artifact_prefix = var.lambda_artifact_prefix
260
+ lambda_artifact_bucket = var.lambda_artifact_bucket
261
+ lambda_artifact_prefix = var.lambda_artifact_prefix
262
+ require_lambda_artifacts = var.require_lambda_artifacts
262
263
 
263
264
  db_cluster_arn = module.database.db_cluster_arn
264
265
  db_cluster_endpoint = module.database.cluster_endpoint
@@ -74,6 +74,11 @@ output "api_endpoint" {
74
74
  value = module.api.api_endpoint
75
75
  }
76
76
 
77
+ output "lambda_artifact_mode" {
78
+ description = "Resolved Lambda artifact source mode: local, s3, or placeholder."
79
+ value = module.api.lambda_artifact_mode
80
+ }
81
+
77
82
  output "api_id" {
78
83
  description = "aws_apigatewayv2_api.main.id — needed by the www-dns module to map api.<domain> onto the HTTP API."
79
84
  value = module.api.api_id