thinkwork-cli 0.9.0 → 0.9.2

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 +2 -2
  3. package/dist/cli.js +1315 -330
  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 +165 -0
  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 +1454 -43
  25. package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
  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 +2 -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 +153 -2
  56. package/dist/terraform/schema.graphql +17 -0
  57. package/package.json +15 -14
@@ -60,6 +60,12 @@ variable "bucket_arn" {
60
60
  type = string
61
61
  }
62
62
 
63
+ variable "enable_workspace_orchestration" {
64
+ description = "Enable S3 EventBridge/SQS routing for workspace file orchestration."
65
+ type = bool
66
+ default = false
67
+ }
68
+
63
69
  variable "user_pool_id" {
64
70
  description = "Cognito user pool ID"
65
71
  type = string
@@ -80,6 +86,12 @@ variable "mobile_client_id" {
80
86
  type = string
81
87
  }
82
88
 
89
+ variable "cognito_auth_domain" {
90
+ description = "Cognito hosted UI domain prefix, e.g. thinkwork-dev. Empty disables MCP OAuth login."
91
+ type = string
92
+ default = ""
93
+ }
94
+
83
95
  variable "appsync_api_url" {
84
96
  description = "AppSync subscriptions endpoint URL"
85
97
  type = string
@@ -134,6 +146,19 @@ variable "api_auth_secret" {
134
146
  default = ""
135
147
  }
136
148
 
149
+ variable "extension_proxy_backends_json" {
150
+ description = "JSON map of enabled Admin extension ids to allowlisted backend base URLs. Example: {\"customer-module\":{\"baseUrl\":\"https://extension.example.com\"}}"
151
+ type = string
152
+ default = "{}"
153
+ }
154
+
155
+ variable "extension_proxy_signing_secret" {
156
+ description = "Shared HMAC secret used by the generic Admin extension proxy to sign actor context for extension backends."
157
+ type = string
158
+ sensitive = true
159
+ default = ""
160
+ }
161
+
137
162
  variable "hindsight_endpoint" {
138
163
  description = "Hindsight API endpoint (empty when enable_hindsight = false)"
139
164
  type = string
@@ -162,12 +187,118 @@ variable "agentcore_function_name" {
162
187
  default = ""
163
188
  }
164
189
 
190
+ variable "agentcore_flue_function_name" {
191
+ description = "Flue AgentCore Lambda function name (for direct SDK invoke); empty until the Flue runtime is provisioned for the stage."
192
+ type = string
193
+ default = ""
194
+ }
195
+
165
196
  variable "agentcore_function_arn" {
166
197
  description = "AgentCore Lambda function ARN (used to grant lambda:InvokeFunction)"
167
198
  type = string
168
199
  default = ""
169
200
  }
170
201
 
202
+ variable "agentcore_flue_function_arn" {
203
+ description = "Flue AgentCore Lambda function ARN (used to grant lambda:InvokeFunction)"
204
+ type = string
205
+ default = ""
206
+ }
207
+
208
+ variable "computer_runtime_cluster_name" {
209
+ description = "ECS cluster name for ThinkWork Computer runtime services"
210
+ type = string
211
+ default = ""
212
+ }
213
+
214
+ variable "computer_runtime_cluster_arn" {
215
+ description = "ECS cluster ARN for ThinkWork Computer runtime services"
216
+ type = string
217
+ default = ""
218
+ }
219
+
220
+ variable "computer_runtime_efs_file_system_id" {
221
+ description = "Shared EFS file system ID for Computer live workspaces"
222
+ type = string
223
+ default = ""
224
+ }
225
+
226
+ variable "computer_runtime_subnet_ids" {
227
+ description = "Subnets for Computer runtime ECS services"
228
+ type = list(string)
229
+ default = []
230
+ }
231
+
232
+ variable "computer_runtime_assign_public_ip" {
233
+ description = "ENABLED or DISABLED for Computer runtime ECS task public IP assignment"
234
+ type = string
235
+ default = "DISABLED"
236
+ validation {
237
+ condition = contains(["ENABLED", "DISABLED"], var.computer_runtime_assign_public_ip)
238
+ error_message = "computer_runtime_assign_public_ip must be ENABLED or DISABLED."
239
+ }
240
+ }
241
+
242
+ variable "computer_runtime_task_sg_id" {
243
+ description = "Security group for Computer runtime ECS tasks"
244
+ type = string
245
+ default = ""
246
+ }
247
+
248
+ variable "computer_runtime_execution_role_arn" {
249
+ description = "ECS execution role ARN for Computer runtime tasks"
250
+ type = string
251
+ default = ""
252
+ }
253
+
254
+ variable "computer_runtime_task_role_arn" {
255
+ description = "ECS task role ARN for Computer runtime tasks"
256
+ type = string
257
+ default = ""
258
+ }
259
+
260
+ variable "computer_runtime_log_group_name" {
261
+ description = "CloudWatch log group for Computer runtime tasks"
262
+ type = string
263
+ default = ""
264
+ }
265
+
266
+ variable "computer_runtime_repository_url" {
267
+ description = "ECR repository URL for the Computer runtime image"
268
+ type = string
269
+ default = ""
270
+ }
271
+
272
+ variable "computer_runtime_default_cpu" {
273
+ description = "Default Fargate CPU units for Computer runtime tasks"
274
+ type = number
275
+ default = 256
276
+ }
277
+
278
+ variable "computer_runtime_default_memory" {
279
+ description = "Default Fargate memory MB for Computer runtime tasks"
280
+ type = number
281
+ default = 512
282
+ }
283
+
284
+ variable "computer_runtime_manager_policy_arn" {
285
+ description = "IAM policy ARN granting Computer manager access to ECS/EFS runtime resources"
286
+ type = string
287
+ default = ""
288
+ }
289
+
290
+ variable "workspace_admin_efs_access_point_arn" {
291
+ description = "Shared EFS access point ARN consumed by the workspace-files-efs Lambda. Rooted at /tenants on the Computer runtime EFS file system so a single Lambda can resolve any (tenantId, computerId) at request time."
292
+ type = string
293
+ default = ""
294
+ }
295
+
296
+ variable "workspace_admin_lambda_sg_id" {
297
+ description = "Security group ID assigned to the workspace-files-efs Lambda. Allowed NFS into the Computer runtime EFS security group via a sibling ingress rule defined in the computer-runtime module."
298
+ type = string
299
+ default = ""
300
+ }
301
+
171
302
  variable "admin_url" {
172
303
  description = "Admin app URL (e.g. https://d3li9vbqnhv7w.cloudfront.net)"
173
304
  type = string
@@ -180,6 +311,24 @@ variable "docs_url" {
180
311
  default = ""
181
312
  }
182
313
 
314
+ variable "www_url" {
315
+ description = "Marketing site URL (e.g. https://thinkwork.ai). Used for Stripe Checkout cancel_url and CORS origin."
316
+ type = string
317
+ default = ""
318
+ }
319
+
320
+ variable "stripe_price_ids_json" {
321
+ description = "JSON object mapping internal plan names to Stripe price IDs for this stage, e.g. {\"starter\":\"price_...\",\"team\":\"price_...\"}. Non-secret; per-stage. Default is an empty object so Lambdas boot even before pricing is configured."
322
+ type = string
323
+ default = "{}"
324
+ }
325
+
326
+ variable "stripe_welcome_from_email" {
327
+ description = "Override From: address on the Stripe post-checkout welcome email. Must be an SES-verified identity. Empty string falls back to the in-code default (hello@agents.thinkwork.ai, which uses the already-verified SES inbound domain). Set to hello@thinkwork.ai once the bare-apex sender identity is verified."
328
+ type = string
329
+ default = ""
330
+ }
331
+
183
332
  variable "appsync_realtime_url" {
184
333
  description = "AppSync realtime/WebSocket endpoint URL"
185
334
  type = string
@@ -204,8 +353,206 @@ variable "job_scheduler_role_arn" {
204
353
  default = ""
205
354
  }
206
355
 
207
- variable "lastmile_tasks_api_url" {
208
- description = "Base URL of the LastMile Tasks REST API used by the outbound sync path (POST /tasks, GET /workflows, etc). Leave blank to feature-flag the integration off; mobile-created tasks then land in sync_status='local' until the URL is set."
356
+ variable "wiki_compile_model_id" {
357
+ description = "Bedrock model id the wiki-compile Lambda uses for the leaf planner, aggregation planner, and section writer. Any Converse-compatible model works. Override per-env if you want to spike a different model without re-deploying code."
358
+ type = string
359
+ default = "openai.gpt-oss-120b-1:0"
360
+ }
361
+
362
+ variable "company_brain_source_agent_model_id" {
363
+ description = "Bedrock model id the GraphQL context-engine Company Brain source-agent runtime uses for JSON tool/action turns. Kept separate from the high-throughput wiki compiler model so source agents can use a model tuned for reliable action JSON."
364
+ type = string
365
+ default = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
366
+ }
367
+
368
+ variable "wiki_aggregation_pass_enabled" {
369
+ description = "Feature flag for the wiki aggregation pass (parent section rollups + section promotion). Pipeline stops after leaf compile when this is off and never populates hub rollups. Stored as a string because the Lambda reads it verbatim from env; must be 'true' / '1' / 'yes' to enable."
370
+ type = string
371
+ default = "true"
372
+ }
373
+
374
+ variable "wiki_deterministic_linking_enabled" {
375
+ description = "Feature flag for deterministic compile-time link emission (parent-expander-driven city/journal references + entity↔entity co-mention edges). When off, the compile pipeline never calls the deterministic linkers and `links_written_deterministic` / `links_written_co_mention` stay at 0 in metrics. Stored as a string because the Lambda reads it verbatim from env; must be 'true' / '1' / 'yes' to enable."
376
+ type = string
377
+ default = "true"
378
+ }
379
+
380
+ variable "google_places_api_key" {
381
+ description = "Google Places API (New) key used by wiki-compile for POI → city/state/country hierarchy enrichment. Stored as a SecureString SSM parameter at /thinkwork/<stage>/google-places/api-key. Empty default creates the parameter with a placeholder value; operator populates via `aws ssm put-parameter --overwrite`. The parameter's value has lifecycle.ignore_changes set so CLI rotation sticks across terraform applies. Compile gracefully degrades when the key is absent (metadata-only place rows) — never fails compile."
382
+ type = string
383
+ default = ""
384
+ sensitive = true
385
+ }
386
+
387
+ # ---------------------------------------------------------------------------
388
+ # Per-user OAuth client credentials
389
+ # ---------------------------------------------------------------------------
390
+
391
+ variable "google_oauth_client_id" {
392
+ description = "Google Workspace OAuth 2.0 client ID (for per-user Gmail/Calendar integration). Stored in Secrets Manager via aws_secretsmanager_secret_version; fetched by Lambdas at cold-start via oauth-client-credentials.ts."
393
+ type = string
394
+ default = ""
395
+ }
396
+
397
+ variable "google_oauth_client_secret" {
398
+ description = "Google Workspace OAuth 2.0 client secret (for per-user Gmail/Calendar integration). Stored in Secrets Manager alongside the client_id; Lambdas fetch both values at cold-start."
399
+ type = string
400
+ default = ""
401
+ sensitive = true
402
+ }
403
+
404
+ variable "redirect_success_url" {
405
+ description = "Default OAuth-callback redirect target used when the caller doesn't supply a returnUrl. Mobile callers pass a thinkwork:// custom scheme; web (admin) falls through to this default."
406
+ type = string
407
+ default = "https://app.thinkwork.ai/settings/credentials"
408
+ }
409
+
410
+ variable "platform_operator_emails" {
411
+ description = "Comma-separated allowlist of emails permitted to invoke operator-gated GraphQL mutations (updateTenantPolicy, sandbox fixture setup, etc.). Compared against ctx.auth.email — pulled from the Cognito JWT for user callers and from the x-principal-email header for service-auth callers. Empty ⇒ the gate rejects every call."
412
+ type = string
413
+ default = ""
414
+ }
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # MCP custom domain (optional) — e.g., mcp.thinkwork.ai
418
+ # ---------------------------------------------------------------------------
419
+
420
+ variable "mcp_custom_domain" {
421
+ description = "Custom domain for the MCP endpoint (e.g., 'mcp.thinkwork.ai'). Empty disables the custom-domain setup entirely. When set, an ACM cert is created; flip mcp_custom_domain_ready=true on the second apply to attach the domain + API mapping after DNS validation completes. See docs/solutions/patterns/mcp-custom-domain-setup-2026-04-23.md for the workflow."
422
+ type = string
423
+ default = ""
424
+ }
425
+
426
+ variable "mcp_custom_domain_ready" {
427
+ description = "Two-apply gate for the MCP custom domain. Leave false on the first apply (cert-only). After running `pnpm cf:sync-mcp` + waiting for ACM validation, flip to true and re-apply to create the API Gateway domain + mapping."
428
+ type = bool
429
+ default = false
430
+ }
431
+
432
+ # ---------------------------------------------------------------------------
433
+ # Routines runtime (Phase B U6) — code-interpreter id for routine-task-python
434
+ # ---------------------------------------------------------------------------
435
+
436
+ variable "agentcore_code_interpreter_id" {
437
+ description = "AgentCore Code Interpreter id used by routine-task-python (Phase B U6) for SFN `python` recipe states. Default empty — the Lambda fails closed with sandbox_misconfigured when unset, which is the correct behavior until Phase B U7 provisions a routines-dedicated interpreter. Operations sets this via tfvars once the interpreter is created."
438
+ type = string
439
+ default = ""
440
+ }
441
+
442
+ variable "routines_execution_role_arn" {
443
+ description = "ARN of the Step Functions execution role newly-created routine state machines run under (Phase B U7). Wired from the routines-stepfunctions module's execution_role_arn output. createRoutine passes this as RoleArn on CreateStateMachine; the lambda-api role's RoutinePassExecutionRole grant is scoped to exactly this ARN."
444
+ type = string
445
+ default = ""
446
+ }
447
+
448
+ variable "routines_log_group_arn" {
449
+ description = "ARN of the routines-stepfunctions CloudWatch log group (Phase B U7). Surfaced to the publish flow for future LoggingConfiguration on CreateStateMachine."
450
+ type = string
451
+ default = ""
452
+ }
453
+
454
+ # ---------------------------------------------------------------------------
455
+ # Phase 3 U4 — compliance-outbox-drainer Aurora credentials
456
+ # ---------------------------------------------------------------------------
457
+
458
+ variable "compliance_drainer_secret_arn" {
459
+ description = "ARN of the Secrets Manager secret holding the `compliance_drainer` Aurora role credentials (Phase 3 U2 / PR #887). Wired from `module.database.compliance_drainer_secret_arn`. The compliance-outbox-drainer Lambda resolves this at module load to connect with INSERT-only access on `compliance.audit_events`."
460
+ type = string
461
+ default = ""
462
+ }
463
+
464
+ # ---------------------------------------------------------------------------
465
+ # Phase 3 U7 — compliance audit-anchor bucket (S3 Object Lock)
466
+ # Default empty until U8a wires the anchor Lambda. The variables exist so
467
+ # U8a can reference them without forcing this PR to also wire a Lambda body.
468
+ # ---------------------------------------------------------------------------
469
+
470
+ variable "compliance_anchor_bucket_arn" {
471
+ description = "ARN of the WORM-protected compliance audit-anchor S3 bucket. Default empty until U8a wires the anchor Lambda."
472
+ type = string
473
+ default = ""
474
+ }
475
+
476
+ variable "compliance_anchor_bucket_name" {
477
+ description = "Name of the WORM-protected compliance audit-anchor S3 bucket. Default empty until U8a wires the anchor Lambda."
478
+ type = string
479
+ default = ""
480
+ }
481
+
482
+ variable "compliance_anchor_lambda_role_arn" {
483
+ description = "ARN of the IAM role the anchor Lambda will assume. Default empty until U8a wires the anchor Lambda."
484
+ type = string
485
+ default = ""
486
+ }
487
+
488
+ variable "compliance_anchor_lambda_role_name" {
489
+ description = "Name of the IAM role the anchor Lambda assumes (extracted from the role resource for inline-policy attachments like the U8a DLQ SendMessage grant)."
490
+ type = string
491
+ default = ""
492
+ }
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # Phase 3 U8a — compliance anchor Lambda runtime config
496
+ # ---------------------------------------------------------------------------
497
+
498
+ variable "compliance_reader_secret_arn" {
499
+ description = "ARN of the Secrets Manager secret holding the `compliance_reader` Aurora role credentials. Wired from `module.database.compliance_reader_secret_arn`. The U8a anchor Lambda uses this for least-privilege SELECT on `compliance.audit_events`."
500
+ type = string
501
+ default = ""
502
+ }
503
+
504
+ variable "compliance_anchor_object_lock_retention_days" {
505
+ description = "Default Object Lock retention for the compliance anchor bucket, in days. Forwarded to the anchor Lambda as COMPLIANCE_ANCHOR_RETENTION_DAYS (consumed by U8b's live function; pre-plumbed in U8a). Master plan baseline: 365."
506
+ type = number
507
+ default = 365
508
+ }
509
+
510
+ # ---------------------------------------------------------------------------
511
+ # Phase 3 U8b — anchor Lambda live config
512
+ # ---------------------------------------------------------------------------
513
+
514
+ variable "compliance_anchor_kms_key_arn" {
515
+ description = "ARN of the customer-managed CMK used for SSE-KMS encryption of anchor objects. Forwarded to the anchor Lambda as COMPLIANCE_ANCHOR_KMS_KEY_ARN (required by `_anchor_fn_live` for the SSE-KMS PutObject)."
516
+ type = string
517
+ default = ""
518
+ }
519
+
520
+ variable "compliance_anchor_object_lock_mode" {
521
+ description = "Object Lock retention mode applied per-object to anchor PutObjects. GOVERNANCE in dev/staging; COMPLIANCE in prod (irreversible). Forwarded to the anchor Lambda as COMPLIANCE_ANCHOR_OBJECT_LOCK_MODE."
522
+ type = string
523
+ default = "GOVERNANCE"
524
+ }
525
+
526
+ variable "compliance_anchor_watchdog_role_arn" {
527
+ description = "ARN of the sibling IAM role the watchdog Lambda assumes (Phase 3 U8b). Decrypt-less: kms:DescribeKey only on the bucket CMK; s3:ListBucket prefix-scoped to anchors/."
528
+ type = string
529
+ default = ""
530
+ }
531
+
532
+ variable "compliance_anchor_watchdog_role_name" {
533
+ description = "Name of the sibling watchdog IAM role. Used for any future inline-policy attachments (e.g., DLQ SendMessage) without re-deriving the name from the ARN."
534
+ type = string
535
+ default = ""
536
+ }
537
+
538
+ # ---------------------------------------------------------------------------
539
+ # Phase 3 U11.U2 — compliance export runner Lambda
540
+ # ---------------------------------------------------------------------------
541
+
542
+ variable "compliance_exports_bucket_name" {
543
+ description = "Name of the compliance-exports S3 bucket. Forwarded to the runner Lambda as COMPLIANCE_EXPORTS_BUCKET so the U11.U3 live body knows where to write CSV/NDJSON artifacts. Default empty — the runner function still deploys but the live body throws on boot."
544
+ type = string
545
+ default = ""
546
+ }
547
+
548
+ variable "compliance_exports_runner_role_arn" {
549
+ description = "ARN of the IAM role the U11 export runner Lambda assumes. Wired from `module.compliance_exports.runner_role_arn`. Default empty until U11.U2 ships."
550
+ type = string
551
+ default = ""
552
+ }
553
+
554
+ variable "compliance_exports_runner_role_name" {
555
+ description = "Name of the IAM role the U11 export runner Lambda assumes (extracted from the role resource for inline-policy attachments — DLQ SendMessage, SQS receive)."
209
556
  type = string
210
557
  default = ""
211
558
  }
@@ -0,0 +1,125 @@
1
+ locals {
2
+ workspace_event_enabled = var.enable_workspace_orchestration && local.use_local_zips
3
+ # EventBridge rejects the more precise per-folder S3 wildcard rules as too
4
+ # complex. Keep broad workspace/catalog families in separate rules and let the
5
+ # dispatcher keep the canonical allowlist in code.
6
+ workspace_event_patterns = {
7
+ workspace = [
8
+ "tenants/*/agents/*/workspace/*",
9
+ ]
10
+ catalog_skills = [
11
+ "tenants/*/agents/_catalog/*/workspace/skills/*",
12
+ ]
13
+ }
14
+ }
15
+
16
+ resource "aws_sqs_queue" "workspace_event_dlq" {
17
+ for_each = local.workspace_event_enabled ? local.workspace_event_patterns : {}
18
+
19
+ name = "thinkwork-${var.stage}-workspace-events-${each.key}-dlq"
20
+ message_retention_seconds = 1209600
21
+ sqs_managed_sse_enabled = true
22
+ }
23
+
24
+ resource "aws_sqs_queue" "workspace_event" {
25
+ for_each = local.workspace_event_enabled ? local.workspace_event_patterns : {}
26
+
27
+ name = "thinkwork-${var.stage}-workspace-events-${each.key}"
28
+ visibility_timeout_seconds = 180
29
+ sqs_managed_sse_enabled = true
30
+
31
+ redrive_policy = jsonencode({
32
+ deadLetterTargetArn = aws_sqs_queue.workspace_event_dlq[each.key].arn
33
+ maxReceiveCount = 1
34
+ })
35
+ }
36
+
37
+ resource "aws_cloudwatch_event_rule" "workspace_event" {
38
+ for_each = local.workspace_event_enabled ? local.workspace_event_patterns : {}
39
+
40
+ name = "thinkwork-${var.stage}-workspace-event-${each.key}"
41
+ description = "Workspace orchestration S3 ${each.key} object events"
42
+
43
+ event_pattern = jsonencode({
44
+ source = ["aws.s3"]
45
+ "detail-type" = ["Object Created", "Object Deleted"]
46
+ detail = {
47
+ bucket = {
48
+ name = [var.bucket_name]
49
+ }
50
+ object = {
51
+ key = [for pattern in each.value : { wildcard = pattern }]
52
+ }
53
+ }
54
+ })
55
+ }
56
+
57
+ resource "aws_cloudwatch_event_target" "workspace_event" {
58
+ for_each = local.workspace_event_enabled ? local.workspace_event_patterns : {}
59
+
60
+ rule = aws_cloudwatch_event_rule.workspace_event[each.key].name
61
+ arn = aws_sqs_queue.workspace_event[each.key].arn
62
+ }
63
+
64
+ resource "aws_sqs_queue_policy" "workspace_event" {
65
+ for_each = local.workspace_event_enabled ? local.workspace_event_patterns : {}
66
+
67
+ queue_url = aws_sqs_queue.workspace_event[each.key].id
68
+ policy = jsonencode({
69
+ Version = "2012-10-17"
70
+ Statement = [
71
+ {
72
+ Effect = "Allow"
73
+ Principal = {
74
+ Service = "events.amazonaws.com"
75
+ }
76
+ Action = "sqs:SendMessage"
77
+ Resource = aws_sqs_queue.workspace_event[each.key].arn
78
+ Condition = {
79
+ ArnEquals = {
80
+ "aws:SourceArn" = aws_cloudwatch_event_rule.workspace_event[each.key].arn
81
+ }
82
+ }
83
+ },
84
+ ]
85
+ })
86
+ }
87
+
88
+ resource "aws_lambda_event_source_mapping" "workspace_event_dispatcher" {
89
+ for_each = local.workspace_event_enabled ? local.workspace_event_patterns : {}
90
+
91
+ event_source_arn = aws_sqs_queue.workspace_event[each.key].arn
92
+ function_name = aws_lambda_function.handler["workspace-event-dispatcher"].arn
93
+ batch_size = 10
94
+ function_response_types = ["ReportBatchItemFailures"]
95
+
96
+ scaling_config {
97
+ maximum_concurrency = 5
98
+ }
99
+ }
100
+
101
+ resource "aws_iam_role_policy" "lambda_workspace_events_sqs" {
102
+ count = local.workspace_event_enabled ? 1 : 0
103
+
104
+ name = "thinkwork-${var.stage}-lambda-workspace-events-sqs"
105
+ role = aws_iam_role.lambda.id
106
+
107
+ policy = jsonencode({
108
+ Version = "2012-10-17"
109
+ Statement = [
110
+ {
111
+ Effect = "Allow"
112
+ Action = [
113
+ "sqs:ReceiveMessage",
114
+ "sqs:DeleteMessage",
115
+ "sqs:GetQueueAttributes",
116
+ "sqs:ChangeMessageVisibility",
117
+ ]
118
+ Resource = concat(
119
+ [for q in aws_sqs_queue.workspace_event : q.arn],
120
+ [for q in aws_sqs_queue.workspace_event_dlq : q.arn],
121
+ )
122
+ },
123
+ ]
124
+ })
125
+ }