thinkwork-cli 0.13.2 → 1.0.0

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 (231) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
  3. package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
  4. package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
  5. package/dist/chunk-MWQ7RO76.js +137 -0
  6. package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
  7. package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
  8. package/dist/cli.js +4400 -16431
  9. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  11. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  12. package/dist/{dist-es-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
  13. package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
  14. package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
  15. package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
  16. package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
  17. package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
  18. package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
  19. package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
  20. package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
  21. package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
  22. package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
  23. package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
  24. package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
  25. package/dist/drizzle/0230_analyst_rls.sql +28 -45
  26. package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
  27. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  28. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  29. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  30. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  31. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  32. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  33. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  34. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  35. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  36. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  37. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  38. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  39. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  40. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  41. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  42. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  43. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  44. package/dist/drizzle/0287_brain_subject.sql +25 -0
  45. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  46. package/dist/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
  47. package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
  48. package/dist/plugins/README.md +2 -2
  49. package/dist/plugins/catalog/package.json +0 -3
  50. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
  51. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  52. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
  53. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
  54. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
  55. package/dist/plugins/catalog/src/contracts.ts +15 -0
  56. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
  57. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  58. package/dist/plugins/twenty/package.json +2 -1
  59. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  60. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  61. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  62. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  63. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  64. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  65. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  66. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  67. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  68. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  69. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  70. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  71. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  72. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  73. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  74. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  75. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  76. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  77. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  78. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  79. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  80. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  81. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  82. package/dist/scripts/post-deploy.sh +79 -1
  83. package/dist/{signin-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
  84. package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
  85. package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
  86. package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
  87. package/dist/terraform/examples/greenfield/main.tf +233 -175
  88. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -8
  89. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  90. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  91. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  92. package/dist/terraform/modules/app/agentcore-identity/main.tf +106 -26
  93. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +22 -7
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
  100. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
  101. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  102. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  103. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  104. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  105. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  106. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  107. package/dist/terraform/modules/app/agentcore-identity/variables.tf +6 -0
  108. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  109. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  110. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  111. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  112. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  113. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  114. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  115. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  116. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  117. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  118. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  119. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  120. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  121. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  122. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  123. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  124. package/dist/terraform/modules/app/deployment-control-plane/runner.py +2641 -124
  125. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
  126. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  127. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  128. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  129. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  130. package/dist/terraform/modules/app/lambda-api/handlers.tf +411 -723
  131. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
  132. package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
  133. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
  134. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  135. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  136. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -45
  137. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  138. package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
  139. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  140. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  141. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  142. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  143. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  144. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  145. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  146. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  147. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  148. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  149. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  150. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  151. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  152. package/dist/terraform/modules/foundation/cognito/main.tf +81 -8
  153. package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
  154. package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
  155. package/dist/terraform/modules/thinkwork/README.md +5 -1
  156. package/dist/terraform/modules/thinkwork/main.tf +376 -374
  157. package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
  158. package/dist/terraform/modules/thinkwork/variables.tf +246 -89
  159. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  160. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  161. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  162. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  163. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  164. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  165. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  166. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  167. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  168. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  169. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  170. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  171. package/package.json +1 -1
  172. package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
  173. package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
  174. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
  175. package/dist/plugins/company-data/README.md +0 -49
  176. package/dist/plugins/company-data/package.json +0 -21
  177. package/dist/plugins/company-data/src/index.ts +0 -30
  178. package/dist/plugins/company-data/src/manifest.ts +0 -26
  179. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  180. package/dist/plugins/company-data/tsconfig.json +0 -8
  181. package/dist/plugins/company-etl/README.md +0 -46
  182. package/dist/plugins/company-etl/package.json +0 -21
  183. package/dist/plugins/company-etl/src/index.ts +0 -29
  184. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  185. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  186. package/dist/plugins/company-etl/tsconfig.json +0 -8
  187. package/dist/plugins/lastmile/README.md +0 -50
  188. package/dist/plugins/lastmile/package.json +0 -23
  189. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  190. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  191. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  192. package/dist/plugins/lastmile/src/index.ts +0 -46
  193. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  194. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  195. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  196. package/dist/plugins/lastmile/tsconfig.json +0 -8
  197. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  198. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  199. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  200. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  201. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  202. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  203. package/dist/terraform/modules/app/agentcore-gateway/main.tf +0 -305
  204. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
  205. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
  206. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
  207. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
  208. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
  209. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
  210. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
  211. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
  212. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
  213. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
  214. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
  215. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
  216. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
  217. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
  218. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
  219. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  220. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  221. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +0 -88
  222. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
  223. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  224. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  225. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  226. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  227. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  228. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  229. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  230. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  231. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -43,15 +43,6 @@ locals {
43
43
  "msteams-install-complete",
44
44
  "msteams-account-link-complete",
45
45
  ],
46
- var.enable_agentcore_multiplayer_proof ? [] : [
47
- "turn-assertion-mint",
48
- "agentcore-proof-oauth-provider",
49
- "agentcore-identity-boundary-target",
50
- "harness-capability-mcp",
51
- "harness-code-interpreter-target",
52
- "harness-builtin-tools-target",
53
- "harness-platform-tools-target",
54
- ],
55
46
  )
56
47
 
57
48
  # Config-class configuration shared by all API handlers. As of plan
@@ -75,33 +66,39 @@ locals {
75
66
  SUBSCRIPTION_TICKET_PUBLIC_KEYS = var.subscription_ticket_public_keys
76
67
  SUBSCRIPTION_TICKET_PRIVATE_KEY_SECRET = var.subscription_ticket_private_key_secret
77
68
  APPSYNC_API_ID = var.appsync_api_id
78
- # THINK-229 U3/KTD5 the analyst policy-source enforcement flip.
79
- # "row" (default) keeps sidecar policy shadow-only; "sidecar" makes the
80
- # signed sidecar block authoritative (budgets/policyClaims flow). Flip
81
- # ONLY after clean shadow parity on live traffic.
82
- ANALYST_POLICY_SOURCE = var.analyst_policy_source
83
- # Governed autonomy — per-tenant opt-in allowlist for autonomous capability
84
- # self-extension (the two self_admit_connection / self_approve_routine
85
- # actions on capability-control-service). Comma/space-separated tenant ids;
86
- # empty (default) = NO tenant enabled, so self-extension ships inert and
87
- # fail-closed. Read via getConfig() (env-wins) in isSelfExtensionEnabled.
88
- CAPABILITY_SELF_EXTENSION_TENANTS = var.capability_self_extension_tenants
89
- # THINK-230 — the operator-facing provisionAnalystConnector mutation runs
90
- # the analyst connector provisioning ceremony inside graphql-http, so the
91
- # shared api handlers read the same broker-secret ARN + rds_iam connect
92
- # config the analyst-query-broker handler carries per-handler. Read via
93
- # getConfig() (env-wins), never process.env (runtime-config fixture gate).
94
- # ANALYST_DB_CLUSTER_ENDPOINT gates on the resource id so a stage without
95
- # the IAM grant leaves rds_iam provisioning off (resolveAnalystRdsIamConfig
96
- # returns null) instead of half-seeding a credential row.
97
- ANALYST_BROKER_SECRET_ARN = var.analyst_broker_secret_arn
98
- ANALYST_DB_CLUSTER_ENDPOINT = var.analyst_db_cluster_resource_id != "" ? var.db_cluster_endpoint : ""
99
- ANALYST_DB_CLUSTER_RESOURCE_ID = var.analyst_db_cluster_resource_id
100
- ANALYST_DB_NAME = var.database_name
101
- ANALYST_DB_USER = "analyst_reader"
102
- DATABASE_SECRET_ARN = var.graphql_db_secret_arn
103
- DATABASE_HOST = var.db_cluster_endpoint
104
- DATABASE_NAME = var.database_name
69
+ # THINK-585 U6 (KTD3): stage kill-switch for AgentCore Runtime chat
70
+ # dispatch. Empty when off (stripped from the document getConfig sees
71
+ # unset). The per-agent agents.agentcore_runtime_dispatch flag gates on
72
+ # top; both must be on for a turn to ride the dispatcher.
73
+ AGENTCORE_RUNTIME_DISPATCH_ENABLED = var.agentcore_runtime_dispatch_enabled ? "true" : ""
74
+ # Neptune endpoint for the
75
+ # identity-graph-projector's nudge gate (THINK-339 U15: the twin READ
76
+ # Lambdas retired to the platform service; the write/projection lane
77
+ # stays product-side the platform identity_sync pipeline invokes it).
78
+ # Document, not env: graphql-http's env sits near the 4KB ceiling.
79
+ # Readers use getConfig; env still wins on the VPC projector.
80
+ NEPTUNE_ENDPOINT = var.neptune_endpoint
81
+ # Consolidation U14: platform-served Brain MCP endpoint. When set,
82
+ # mcp-twin-provision registers the twin connector against it instead
83
+ # of the product /mcp/twin route. Empty = legacy default.
84
+ BRAIN_MCP_URL = var.brain_mcp_url
85
+ # THINK-781: Brain ops-api base URL + the agent-identity m2m secret the
86
+ # flagThreadToBrain resolver posts /flags with. NOT the /mcp lane pair
87
+ # above POST /flags verifies against the Brain's operator pool, so it
88
+ # needs the etl-platform/agent/cognito-m2m-brain credential (scope
89
+ # etl-agent/tasks), not the brain-mcp platform-agent lane. Empty = the
90
+ # "Send to the Brain" action fails with "no Brain connection configured".
91
+ BRAIN_OPS_API_URL = var.brain_ops_api_url
92
+ BRAIN_OPS_M2M_SECRET_ARN = var.brain_ops_m2m_secret_arn
93
+ # Same class of bug, second instance (2026-07-22): a post-apply
94
+ # twin-export hook read BRAIN_ARTIFACTS_BUCKET from env it never had
95
+ # and silently returned skipped_no_bucket on every apply.
96
+ # Document-served so every api handler resolves it via getConfig; the
97
+ # handlers that already carry it as env keep winning.
98
+ BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
99
+ DATABASE_SECRET_ARN = var.graphql_db_secret_arn
100
+ DATABASE_HOST = var.db_cluster_endpoint
101
+ DATABASE_NAME = var.database_name
105
102
  # THINK-280 U8: the LAST rollout gate. External capability search + the
106
103
  # Inspector governed-runtime operation layer stay inert until the broker is
107
104
  # enabled for the stage. Off by default — read via getConfig() (env-wins).
@@ -120,9 +117,17 @@ locals {
120
117
  COGNITO_AUTH_BASE_URL = local.mcp_oauth_cognito_base_url
121
118
  MCP_OAUTH_CALLBACK_URL = "${local.mcp_oauth_api_base_url}/mcp/oauth/callback"
122
119
  MCP_OAUTH_REVOCATIONS_TABLE = aws_dynamodb_table.mcp_oauth_revocations.name
123
- COGNITO_APP_CLIENT_IDS = "${var.admin_client_id},${var.mobile_client_id}"
124
- APPSYNC_ENDPOINT = var.appsync_api_url
125
- THINKWORK_API_URL = local.api_base_url
120
+ # THINK-643: backing store for short-lived auth-flow state. While this is
121
+ # "postgres" the auth handlers never touch DynamoDB and AUTH_STATE_TABLE is
122
+ # empty (no table exists). Read via getConfig() — never process.env.
123
+ AUTH_STATE_STORE = var.auth_state_store
124
+ AUTH_STATE_TABLE = local.auth_state_enabled ? aws_dynamodb_table.auth_state[0].name : ""
125
+ # R16: comma-separated app-client allowlist. The optional console client id
126
+ # (U13) joins only when enabled; compact() drops the empty placeholder so
127
+ # the value is byte-identical to before while the console is off.
128
+ COGNITO_APP_CLIENT_IDS = join(",", compact([var.admin_client_id, var.mobile_client_id, var.console_client_id]))
129
+ APPSYNC_ENDPOINT = var.appsync_api_url
130
+ THINKWORK_API_URL = local.api_base_url
126
131
  # Deterministic routines v1 (plan 2026-07-03-004 U6): activates the
127
132
  # git-backed routine lifecycle tool suite on the admin-ops MCP server
128
133
  # (routine_repo_list/read/commit, routine_run_fixtures, routine_runs).
@@ -156,13 +161,11 @@ locals {
156
161
  # WORKSPACE_RENDERER_FUNCTION_NAME is derived from the per-stage naming
157
162
  # convention by deriveFunctionName("workspace-renderer") — stored
158
163
  # nowhere (R7).
159
- WORKSPACE_BUCKET = var.bucket_name
160
- HINDSIGHT_ENDPOINT = var.hindsight_endpoint
161
- # THINK-220 cutover flag: empty = hindsight schema on the primary DB;
162
- # set = that database's public schema via the database-pg seam.
163
- HINDSIGHT_DATABASE_NAME = var.hindsight_database_name
164
- AGENTCORE_MEMORY_ID = var.agentcore_memory_id
165
- MEMORY_ENGINE = var.memory_engine
164
+ WORKSPACE_BUCKET = var.bucket_name
165
+ AGENTCORE_MEMORY_ID = var.agentcore_memory_id
166
+ # AgentCore managed memory is the only engine (THINK-407). The env var
167
+ # stays so the API's memory config seam keeps an explicit value.
168
+ MEMORY_ENGINE = "agentcore"
166
169
  # CHAT_AGENT_INVOKE_FN_ARN (~112 serialized bytes) was dropped for the
167
170
  # 4KB env ceiling (#2375): getChatAgentInvokeFnArn and managed-dispatch
168
171
  # now derive the ARN from the deterministic naming pattern
@@ -178,7 +181,6 @@ locals {
178
181
  # deploymentStatus reads ADMIN_URL directly — so the single canonical
179
182
  # ADMIN_URL is sufficient. Re-add aliases only after env vars move to SSM.
180
183
  ADMIN_URL = var.admin_url
181
- DOCS_URL = var.docs_url
182
184
  APPSYNC_REALTIME_URL = var.appsync_realtime_url
183
185
  ECR_REPOSITORY_URL = var.ecr_repository_url
184
186
  # Per-user OAuth wiring (Google Workspace today; Microsoft 365 follow-up).
@@ -264,20 +266,7 @@ locals {
264
266
  # THINK-316 U1: the assertion mint runs on a sibling role and receives only
265
267
  # the database connection plus immutable stage identity. In particular it
266
268
  # does not inherit API_AUTH_SECRET or APPSYNC_API_KEY from common_env.
267
- turn_assertion_mint_env = {
268
- STAGE = var.stage
269
- DATABASE_URL = "postgresql://${var.db_username}:${urlencode(var.db_password)}@${var.db_cluster_endpoint}:5432/${var.database_name}?sslmode=no-verify"
270
- AWS_ACCOUNT_ID = var.account_id
271
- NODE_OPTIONS = "--enable-source-maps"
272
- }
273
269
 
274
- # THINK-316 U1: the synthetic provider/target are proof-only public
275
- # boundaries. They receive no database URL, platform bearer, or AppSync key.
276
- agentcore_proof_boundary_env = {
277
- STAGE = var.stage
278
- AWS_ACCOUNT_ID = var.account_id
279
- NODE_OPTIONS = "--enable-source-maps"
280
- }
281
270
 
282
271
  # Per-handler env-var overrides. ARNs are constructed from the naming
283
272
  # pattern (same trick as the api-cross-function-invoke statement in
@@ -292,70 +281,12 @@ locals {
292
281
  }
293
282
 
294
283
  handler_extra_env = {
295
- # THINK-316 U1: short-lived, server-minted identity assertions for
296
- # AgentCore Harness and Gateway. The public mcp-oauth handler receives the
297
- # key id only to publish GetPublicKey-derived JWKS; its role cannot Sign.
298
- "mcp-oauth" = {
299
- AGENTCORE_TURN_ASSERTION_ISSUER = local.agentcore_turn_assertion_issuer
300
- AGENTCORE_TURN_ASSERTION_JWKS_KEYS = jsonencode([
301
- for key in values(local.agentcore_turn_assertion_keys) : {
302
- keyId = key.key_id
303
- kid = key.kid
304
- }
305
- ])
306
- }
307
284
  "skills" = {
308
285
  AGENTCORE_IDENTITY_WORKLOAD_NAME = "thinkwork-${var.stage}-multiplayer-proof"
309
286
  AGENTCORE_TWENTY_CREDENTIAL_PROVIDER_NAME = "thinkwork-${var.stage}-twenty-crm"
310
287
  AGENTCORE_USER_OAUTH_RETURN_URL = "${local.mcp_oauth_api_base_url}/api/skills/mcp-oauth/agentcore/complete"
311
288
  AGENTCORE_TWENTY_OAUTH_RESOURCE = "https://crm.thinkwork.ai/mcp"
312
289
  }
313
- "turn-assertion-mint" = {
314
- AGENTCORE_TURN_ASSERTION_ISSUER = local.agentcore_turn_assertion_issuer
315
- AGENTCORE_HARNESS_AUDIENCE = local.agentcore_harness_audience
316
- AGENTCORE_GATEWAY_AUDIENCE = local.agentcore_gateway_audience
317
- AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.agentcore_turn_assertion_active_key.key_id
318
- AGENTCORE_TURN_ASSERTION_KID = local.agentcore_turn_assertion_active_key.kid
319
- }
320
- "agentcore-proof-oauth-provider" = {
321
- AGENTCORE_PROOF_OAUTH_ISSUER = "${local.mcp_oauth_api_base_url}/agentcore-proof/oauth"
322
- AGENTCORE_PROOF_OAUTH_CLIENT_ID = var.agentcore_proof_oauth_client_id
323
- AGENTCORE_PROOF_OAUTH_CLIENT_SECRET = var.agentcore_proof_oauth_client_secret
324
- AGENTCORE_ASSERTION_ISSUER = local.agentcore_turn_assertion_issuer
325
- AGENTCORE_HARNESS_AUDIENCE = local.agentcore_harness_audience
326
- AGENTCORE_GATEWAY_AUDIENCE = local.agentcore_gateway_audience
327
- AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.agentcore_turn_assertion_active_key.key_id
328
- AGENTCORE_TURN_ASSERTION_KID = local.agentcore_turn_assertion_active_key.kid
329
- AGENTCORE_PROOF_OWNER_ALLOWLIST = var.agentcore_proof_owner_allowlist
330
- }
331
- "agentcore-identity-boundary-target" = {
332
- AGENTCORE_PROOF_OAUTH_ISSUER = "${local.mcp_oauth_api_base_url}/agentcore-proof/oauth"
333
- AGENTCORE_PROOF_OAUTH_CLIENT_SECRET = var.agentcore_proof_oauth_client_secret
334
- AGENTCORE_PROOF_OWNER_ALLOWLIST = var.agentcore_proof_owner_allowlist
335
- }
336
- "harness-capability-mcp" = {
337
- AGENTCORE_PROOF_OAUTH_ISSUER = "${local.mcp_oauth_api_base_url}/agentcore-proof/oauth"
338
- AGENTCORE_PROOF_OAUTH_CLIENT_SECRET = var.agentcore_proof_oauth_client_secret
339
- AGENTCORE_GATEWAY_POLICY_REVISION = "mcp-list-call-v2-tenant-admission"
340
- AGENTCORE_IDENTITY_WORKLOAD_NAME = "thinkwork-${var.stage}-multiplayer-proof"
341
- AGENTCORE_TWENTY_CREDENTIAL_PROVIDER_NAME = "thinkwork-${var.stage}-twenty-crm"
342
- AGENTCORE_TWENTY_OAUTH_RESOURCE = "https://crm.thinkwork.ai/mcp"
343
- }
344
- "harness-code-interpreter-target" = {
345
- AGENTCORE_PROOF_OAUTH_ISSUER = "${local.mcp_oauth_api_base_url}/agentcore-proof/oauth"
346
- AGENTCORE_PROOF_OAUTH_CLIENT_SECRET = var.agentcore_proof_oauth_client_secret
347
- AGENTCORE_GATEWAY_POLICY_REVISION = "sandbox-execute-v1-tenant-admission"
348
- }
349
- "harness-builtin-tools-target" = {
350
- AGENTCORE_PROOF_OAUTH_ISSUER = "${local.mcp_oauth_api_base_url}/agentcore-proof/oauth"
351
- AGENTCORE_PROOF_OAUTH_CLIENT_SECRET = var.agentcore_proof_oauth_client_secret
352
- AGENTCORE_GATEWAY_POLICY_REVISION = "builtin-web-v1-tenant-admission"
353
- }
354
- "harness-platform-tools-target" = {
355
- AGENTCORE_PROOF_OAUTH_ISSUER = "${local.mcp_oauth_api_base_url}/agentcore-proof/oauth"
356
- AGENTCORE_PROOF_OAUTH_CLIENT_SECRET = var.agentcore_proof_oauth_client_secret
357
- AGENTCORE_GATEWAY_POLICY_REVISION = "platform-tools-v4-user-questions"
358
- }
359
290
  "workos-auth" = {
360
291
  # During cutover/soak the callback, bridge, and logout paths remain for
361
292
  # already-issued state, but the handler independently refuses every new
@@ -371,40 +302,6 @@ locals {
371
302
  "auth-enrollment" = {
372
303
  AUTH_MIGRATION_RECOVERY_DEADLINE = var.auth_migration_recovery_deadline
373
304
  }
374
- # Analyst query broker (THINK-228 U3). Reader role + caller credential
375
- # secrets, and the workspace bucket's analyst-staging/ prefix for
376
- # large-result CSVs (lifecycle TTL lives on the bucket module). The
377
- # shared writer DATABASE_SECRET_ARN also lands in this handler's env
378
- # via common_env, but the broker code never uses it — SQL runs only as
379
- # analyst_reader.
380
- "analyst-query-broker" = {
381
- ANALYST_READER_SECRET_ARN = var.analyst_reader_secret_arn
382
- ANALYST_BROKER_SECRET_ARN = var.analyst_broker_secret_arn
383
- ANALYST_STAGING_BUCKET = var.bucket_name
384
- ANALYST_STAGING_PREFIX = "analyst-staging"
385
- # THINK-229 U1: RDS IAM connect config. Endpoint presence switches
386
- # analyst-reader-db.ts to the IAM-token path (password secret above
387
- # is the pre-GRANT-rds_iam fallback, retired once IAM is proven).
388
- # Gated on the resource ID so a stage without the IAM grant keeps
389
- # the password path instead of failing into the fallback every cold
390
- # start.
391
- ANALYST_DB_CLUSTER_ENDPOINT = var.analyst_db_cluster_resource_id != "" ? var.db_cluster_endpoint : ""
392
- ANALYST_DB_CLUSTER_RESOURCE_ID = var.analyst_db_cluster_resource_id
393
- ANALYST_DB_NAME = var.database_name
394
- ANALYST_DB_USER = "analyst_reader"
395
- }
396
- # THINK-229 U5 — the connection reconciler probes the analyst_reader
397
- # connection EXACTLY as the broker does (getAnalystReaderClient), so it
398
- # needs the same IAM-connect config + the password-fallback secret. The
399
- # shared lambda execution role already holds rds-db:connect on the
400
- # analyst_reader dbuser (iam-grouped.tf, granted in U1), so no extra IAM.
401
- "analyst-connection-reconciler" = {
402
- ANALYST_READER_SECRET_ARN = var.analyst_reader_secret_arn
403
- ANALYST_DB_CLUSTER_ENDPOINT = var.analyst_db_cluster_resource_id != "" ? var.db_cluster_endpoint : ""
404
- ANALYST_DB_CLUSTER_RESOURCE_ID = var.analyst_db_cluster_resource_id
405
- ANALYST_DB_NAME = var.database_name
406
- ANALYST_DB_USER = "analyst_reader"
407
- }
408
305
  # THINK-246: customer stages cannot send as the dev fallback domain
409
306
  # (noreply@agents.thinkwork.ai is only verified in the dev account) —
410
307
  # observed live on TEI as ses_send_failed on the deliver step.
@@ -447,46 +344,6 @@ locals {
447
344
  CAPABILITY_BROKER_VPCE_DNS = var.capability_broker_vpce_dns
448
345
  CAPABILITY_BROKER_SESSION_TABLE = var.capability_broker_session_table
449
346
  }
450
- # Compounding Memory compile Lambda. Any Converse-compatible Bedrock
451
- # model works; the planner + section-writer cap themselves at ~500
452
- # records / 25 new pages per invocation so a 480 s timeout covers
453
- # the worst case comfortably. Env vars come from variables so
454
- # unrelated deploys don't wipe them back to defaults (the aggregation
455
- # flag got reset on every terraform apply before this was pinned).
456
- "wiki-compile" = {
457
- BEDROCK_MODEL_ID = var.wiki_compile_model_id
458
- WIKI_AGGREGATION_PASS_ENABLED = var.wiki_aggregation_pass_enabled
459
- WIKI_DETERMINISTIC_LINKING_ENABLED = var.wiki_deterministic_linking_enabled
460
- # Wiki pipeline source dispatch (plan 2026-06-09-004 U10):
461
- # 'planner' (default, LLM compile) | 'graph' (deterministic
462
- # graph→wiki materializer over the knowledge-graph mirror).
463
- WIKI_SOURCE = var.wiki_source
464
- # Name (not value) of the SecureString SSM parameter that holds the
465
- # Google Places API key. wiki-compile fetches + caches on cold start.
466
- # The parameter may contain a placeholder value at apply time — the
467
- # Lambda logs and degrades gracefully if decryption returns empty.
468
- GOOGLE_PLACES_SSM_PARAM_NAME = "/thinkwork/${var.stage}/google-places/api-key"
469
- # THINK-200 chain: a successful compile Event-invokes okf-materialize
470
- # so the OKF projection (and, chained, the Pi navigator's EFS view)
471
- # stays current without manual invocation.
472
- OKF_MATERIALIZE_FN_NAME = "thinkwork-${var.stage}-api-okf-materialize"
473
- }
474
- "ontology-scan" = {
475
- BEDROCK_MODEL_ID = var.wiki_compile_model_id
476
- }
477
- "wiki-export" = {
478
- WIKI_EXPORT_BUCKET = aws_s3_bucket.wiki_exports.bucket
479
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
480
- }
481
- "okf-materialize" = {
482
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
483
- # THINK-200 chain: fresh bundles fan out to the EFS current view.
484
- OKF_EFS_REFRESH_FN_NAME = "thinkwork-${var.stage}-api-okf-efs-refresh"
485
- }
486
- "okf-efs-refresh" = {
487
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
488
- OKF_EFS_ROOT = var.okf_efs_mount_path
489
- }
490
347
  # THINK-193 U1 (Codex F6): normalized evidence snapshots live in the
491
348
  # encrypted brain-artifacts bucket under evidence-snapshots/ — Postgres
492
349
  # keeps only the s3:// ref + hash. 30-day lifecycle expiration below.
@@ -517,58 +374,72 @@ locals {
517
374
  CONTEXT_ENGINE_MEMORY_QUERY_MODE = "reflect"
518
375
  CONTEXT_ENGINE_MEMORY_TIMEOUT_MS = "20000"
519
376
  }
520
- # Agent graph access (plan 2026-06-09-004 U8): stage gate for the Pi
521
- # knowledge_graph_search tool; the per-agent tool policy gates on top.
377
+ "agentcore-runtime-dispatch" = {
378
+ # THINK-585 U6: the dispatcher resolves the Pi runtime id from SSM
379
+ # (5-min cache) and derives the ARN from identity env. Stage-scoped
380
+ # param name is config, not identity — env is the right layer.
381
+ AGENTCORE_PI_RUNTIME_SSM_NAME = "/thinkwork/${var.stage}/agentcore/runtime-id-pi"
382
+ # THINK-909: runtime session scope. "thread" (default) keys the
383
+ # AgentCore session per thread — today's behavior. "user" keys it per
384
+ # (tenant, agent, user) so a new thread reuses the warm microVM, with
385
+ # an immediate per-thread fallback on a 409. Flip a stage to "user"
386
+ # ONLY after its Pi runtime image dual-accepts both session ids.
387
+ AGENTCORE_SESSION_SCOPE = var.agentcore_session_scope
388
+ }
522
389
  "chat-agent-invoke" = {
523
- KNOWLEDGE_GRAPH_TOOL_ENABLED = tostring(var.knowledge_graph_tool_enabled)
390
+ # THINK-324 C18: mint the signed-turn assertion with the active key.
391
+ AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.turn_assertion_active_key_arn
392
+ # THINK-321 U5: stage gate for the Pi identity-resolution tools; the
393
+ # per-agent tool policy gates on top. Mirrored on wakeup-processor so
394
+ # wakeup turns carry the same payload flag (env-gated feature is dead
395
+ # in deployed stacks without the terraform var).
396
+ IDENTITY_RESOLUTION_ENABLED = tostring(var.identity_resolution_tool_enabled)
524
397
  # THINK-311 U5: no HARNESS_RUNNER_FUNCTION_NAME env — derivable
525
398
  # thinkwork-<stage>-api-* names never ride env (R1/R10 identity-only
526
399
  # rule); resolveRuntimeFunctionName derives it from STAGE at call
527
400
  # time, exactly like workspace-renderer.
528
401
  }
402
+ # THINK-321 U5: wakeup turns read the same identity-resolution stage
403
+ # gate as chat-agent-invoke (both payload builders emit
404
+ # identity_resolution_enabled).
405
+ "wakeup-processor" = {
406
+ # THINK-324 C18: wakeup dispatches mint the same signed-turn assertion.
407
+ AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.turn_assertion_active_key_arn
408
+ IDENTITY_RESOLUTION_ENABLED = tostring(var.identity_resolution_tool_enabled)
409
+ }
410
+ # THINK-324 C18/C19: the ledger endpoint verifies presented assertions
411
+ # with the cached public key of the same active mint key. Required mode:
412
+ # its only producer (the Pi tool-execution emitter) echoes the assertion
413
+ # since C18, so assertion-less ledger writes are refused. Verifier
414
+ # unavailability still degrades to bearer-only (never a write outage);
415
+ # a mint failure at dispatch costs that turn's evidence, logged loudly.
416
+ "tool-executions" = {
417
+ AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.turn_assertion_active_key_arn
418
+ TURN_ASSERTION_REQUIRED = "true"
419
+ }
420
+ # THINK-324 C19: activity + finalize verify presented assertions
421
+ # (tolerant posture — legacy producers don't echo yet).
422
+ "chat-agent-activity" = {
423
+ AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.turn_assertion_active_key_arn
424
+ }
425
+ "chat-agent-finalize" = {
426
+ AGENTCORE_TURN_ASSERTION_KMS_KEY_ID = local.turn_assertion_active_key_arn
427
+ }
529
428
  # THINK-316 U5: the runner reads the server-only attested profile and
530
429
  # invokes its named endpoint with a purpose-bound CUSTOM_JWT. It receives
531
430
  # no Harness control-plane inputs or permissions.
532
- "harness-runner" = {}
533
- # 240s: sync Hindsight retain (LLM extraction + auto-consolidation) can
534
- # exceed 60s; the client timeout must stay below the Lambda timeout (300s)
535
- # and below the Hindsight ALB idle_timeout (300s) so failures classify as
536
- # client timeouts, never ALB 504s.
537
- "memory-retain" = {
538
- HINDSIGHT_TIMEOUT_MS = "240000"
539
- }
540
- "brain-dream-state" = {
541
- BRAIN_DREAM_STATE_ENABLED = tostring(var.brain_dream_state_enabled)
542
- }
543
- # Bedrock KB provisioning. Per-handler (not common_env) so these don't bloat
544
- # the already-near-4KB graphql-http env. Bedrock's RDS-backed KB needs the
545
- # cluster ARN + the KB service role (passed at CreateKnowledgeBase time).
546
- "knowledge-base-manager" = {
547
- KB_SERVICE_ROLE_ARN = var.kb_service_role_arn
548
- DATABASE_CLUSTER_ARN = var.db_cluster_arn
549
- }
550
- # Observations → Knowledge Graph worker. Extraction is now a Bedrock
551
- # structured-output call inside this Lambda. KG_EXTRACTION_MODEL_ID pins
552
- # the gpt-oss extraction model (Bedrock IAM via the shared lambda_bedrock
553
- # invoke policy, same as the promotion-gate classifier).
554
- "knowledge-graph-observations-ingest" = {
555
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
556
- OBSERVATION_CLASSIFIER_MODEL_ID = var.observation_classifier_model_id
557
- KG_EXTRACTION_MODEL_ID = var.kg_extraction_model_id
558
- # Per-run candidate cap bounds classifier + extraction cost; a
559
- # truncated backlog drains via an in-process loop inside one
560
- # invocation (never Lambda self-invoke — AWS recursive-loop
561
- # detection terminates worker-to-self Event chains).
562
- KG_OBS_MAX_CANDIDATES_PER_RUN = var.kg_obs_max_candidates_per_run
563
- # THINK-193 U4 dead-handoff fix: a successful observations ingest
564
- # enqueues the tenant graph wiki-compile job inside its own commit
565
- # and Event-invokes wiki-compile post-commit. WIKI_SOURCE must match
566
- # the wiki-compile handler's dispatch flag (stage-global); the
567
- # per-tenant kill switch stays tenants.wiki_compile_enabled. The
568
- # invoke rides the shared lambda role's existing wiki-compile
569
- # lambda:InvokeFunction grant (iam-grouped.tf); the function name is
570
- # derived from STAGE (common_env) at call time.
571
- WIKI_SOURCE = var.wiki_source
431
+ # 240s: a sync memory retain (LLM extraction + consolidation) can exceed
432
+ # 60s; the client timeout must stay below the Lambda timeout.
433
+ # Company Brain U5: Neptune coordinates for the twin graph projector.
434
+ # Empty endpoint leaves the projector inert (nudge helper skips too).
435
+ "identity-graph-projector" = {
436
+ NEPTUNE_ENDPOINT = var.neptune_endpoint
437
+ NEPTUNE_PORT = "8182"
438
+ BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
439
+ # Bulk-rebuild lane (THINK-331): loader staging coordinates. Empty =
440
+ # bulk-rebuild mode returns a structured "not configured" error.
441
+ NEPTUNE_LOAD_BUCKET = var.neptune_load_bucket
442
+ NEPTUNE_LOADER_ROLE_ARN = var.neptune_loader_role_arn
572
443
  }
573
444
  # routine-task-python (Phase B U6) needs the AgentCore code-interpreter
574
445
  # id + the per-stage S3 routine-output bucket. The interpreter id is
@@ -704,10 +575,17 @@ resource "aws_lambda_function" "skill_trust_runner" {
704
575
  # Helper: creates Lambda functions from a local zip directory or release S3 keys
705
576
  # ---------------------------------------------------------------------------
706
577
 
707
- resource "aws_lambda_function" "handler" {
708
- for_each = local.deploy_lambda_handlers ? setsubtract(toset([
578
+ # The set of api handlers this module deploys (the for_each source for
579
+ # aws_lambda_function.handler). Hoisted into a local so the per-handler VPC
580
+ # placement map below (THINK-1082) can be built over the same names.
581
+ locals {
582
+ handler_names = local.deploy_lambda_handlers ? setsubtract(toset([
709
583
  "graphql-http",
710
584
  "chat-agent-invoke",
585
+ # THINK-585 U6: thin waiting dispatcher (InvokeAgentRuntime, held
586
+ # connection) + its DLQ redrive consumer (see dispatch-dlq.tf).
587
+ "agentcore-runtime-dispatch",
588
+ "agentcore-dispatch-dlq-redrive",
711
589
  # Mobile agent harness: cloud Bedrock Converse proxy + completed-turn
712
590
  # persistence. Routes for these live in local.api_routes; the function
713
591
  # names must also be listed here (this set is the for_each source for
@@ -728,13 +606,6 @@ resource "aws_lambda_function" "handler" {
728
606
  # Client Engagement app API. Calls Twenty REST API directly server-side;
729
607
  # it is not backed by ThinkWork GraphQL or the agent MCP runtime.
730
608
  "twenty-client-engagement",
731
- # Desktop-local Pi tombstone endpoints. Kept temporarily so old packaged
732
- # desktop clients receive a stable 410 while all supported Pi execution
733
- # routes through managed AgentCore.
734
- "desktop-runtime-session",
735
- "desktop-workspace-prewarm",
736
- "managed-delegation",
737
- "desktop-eval-runs",
738
609
  # chat-agent-finalize — POST /api/threads/{threadId}/finalize. The
739
610
  # AgentCore runtime POSTs here at end-of-turn so the post-AgentCore
740
611
  # bookkeeping (cost recording, message insert, AppSync notify,
@@ -743,23 +614,14 @@ resource "aws_lambda_function" "handler" {
743
614
  # Bearer API_AUTH_SECRET. Idempotent on thread_turns.finalized_at
744
615
  # (migration 0123). Plan: 2026-05-22-006.
745
616
  "chat-agent-finalize",
746
- # harness-runner — THINK-311 U5: Event-mode target for harness-flagged
747
617
  # agents. The chat dispatch selector (resolveRuntimeFunctionName) routes
748
618
  # a trial agent's turn here instead of the Pi container Lambda; the
749
619
  # runner projects the payload into an AWS AgentCore Harness config,
750
620
  # drives InvokeHarness, fulfills emit_document, and finalizes the turn
751
621
  # via processFinalize. No API route — direct Event invoke only.
752
- "harness-runner",
753
622
  # THINK-316 U1: direct-invoke-only assertion mint. It derives all identity
754
623
  # claims from the persisted turn tuple and signs with a dedicated KMS key;
755
624
  # there is intentionally no public API Gateway route.
756
- "turn-assertion-mint",
757
- "agentcore-proof-oauth-provider",
758
- "agentcore-identity-boundary-target",
759
- "harness-capability-mcp",
760
- "harness-code-interpreter-target",
761
- "harness-builtin-tools-target",
762
- "harness-platform-tools-target",
763
625
  # canvas-refresh — headless Living Artifacts data-refresh (THINK-145 U6).
764
626
  # Invoked RequestResponse by the refreshCanvasData mutation (graphql-http)
765
627
  # and by job-trigger's canvas_refresh branch (U7). Re-runs the saved
@@ -815,6 +677,9 @@ resource "aws_lambda_function" "handler" {
815
677
  "mcp-open-engine",
816
678
  # THINK-280 U8: scoped external capability search MCP facade.
817
679
  "mcp-capability-search",
680
+ # THINK-333 / consolidation U14: provisions the tenant Brain MCP
681
+ # connector against the platform endpoint (BRAIN_MCP_URL).
682
+ "mcp-twin-provision",
818
683
  "activity",
819
684
  "routines",
820
685
  "budgets",
@@ -829,11 +694,6 @@ resource "aws_lambda_function" "handler" {
829
694
  "webhooks-admin",
830
695
  "webhook-deliveries-cleanup",
831
696
  "skill-runs-reconciler",
832
- # THINK-229 U5 — probes the analyst_reader connection (reachability, IAM
833
- # auth, SELECT-grant introspection, schema drift; all read-only) every 30
834
- # min and stamps the verdict onto runtime_metadata.analyst_probe so
835
- # dispatch can withhold a failing/stale connection loudly.
836
- "analyst-connection-reconciler",
837
697
  "cron-stall-monitor",
838
698
  "webhook-crm-opportunity",
839
699
  "webhook-task-event",
@@ -842,8 +702,6 @@ resource "aws_lambda_function" "handler" {
842
702
  # 2026-06-12-002 U4). Bearer API_AUTH_SECRET + x-tenant-id; the runtime
843
703
  # fetch tool authorizes here, then downloads the returned S3 keys itself.
844
704
  "workspace-fetch-source",
845
- "knowledge-base-manager",
846
- "knowledge-base-files",
847
705
  "email-send",
848
706
  "email-inbound",
849
707
  "email-provider-webhook",
@@ -854,9 +712,7 @@ resource "aws_lambda_function" "handler" {
854
712
  "msteams-install-complete",
855
713
  "msteams-account-link-complete",
856
714
  "github-app",
857
- "memory",
858
715
  "memory-retain",
859
- "brain-dream-state",
860
716
  "memory-stage-worker",
861
717
  # THINK-193 U2 (Codex P1 #3): scheduled retry drainer for the retraction
862
718
  # saga ledger. Claims due/stale memory_retraction_attempts rows with the
@@ -871,15 +727,15 @@ resource "aws_lambda_function" "handler" {
871
727
  # terminal step event + SendTaskFailure. rate(15 minutes) schedule
872
728
  # (dedicated resource below).
873
729
  "memory-stage-sweeper",
874
- "wiki-compile",
875
- "knowledge-graph-observations-ingest",
876
- "ontology-scan",
877
- "ontology-reprocess",
878
- "wiki-lint",
879
- "wiki-export",
880
- "okf-materialize",
881
- "okf-efs-refresh",
882
- "wiki-bootstrap-import",
730
+ # THINK-321 U7: bootstrap/drift identity matching job. Event-invoked by
731
+ # startIdentityMatchJob; the drift EventBridge Scheduler rule targets it
732
+ # directly (identity_drift_match_enabled, ships DISABLED).
733
+ "identity-match",
734
+ # Company Brain U5 (KTD-4): identity → twin graph projector. VPC-attached
735
+ # (Neptune). Event-invoked as a post-commit nudge by identity mutations;
736
+ # RequestResponse for the rebuild command. The per-tenant cursor row is
737
+ # the source of truth — missed nudges are harmless.
738
+ "identity-graph-projector",
883
739
  "artifact-deliver",
884
740
  "recipe-refresh",
885
741
  "agent-skills-list",
@@ -966,16 +822,14 @@ resource "aws_lambda_function" "handler" {
966
822
  # Admin-Ops MCP — JSON-RPC endpoint at POST /mcp/admin, exposes the
967
823
  # @thinkwork/admin-ops package as MCP tools for managed agents.
968
824
  "admin-ops-mcp",
969
- # Analyst query broker — first-party MCP server at POST /mcp/analyst
970
- # (THINK-228 U3). One tool, query: EXPLAIN-gated, extended-protocol
971
- # single-statement SQL as the analyst_reader role; large results stage
972
- # to S3 under analyst-staging/; every query emits a data.query_executed
973
- # compliance audit event via POST /api/compliance/events.
974
- "analyst-query-broker",
975
825
  # MCP admin key management — per-tenant Bearer tokens for admin-ops.
976
826
  # Admin-ops-mcp authenticates incoming tokens by sha256-hash lookup
977
827
  # against tenant_mcp_admin_keys, populated by this handler's routes.
978
828
  "mcp-admin-keys",
829
+ # Brain API key management — user-minted `tkt_` Bearer tokens for the
830
+ # platform Company Brain MCP. Create/revoke republish the hashed-key
831
+ # manifest the platform verifier reads (twin-mcp-keys/<tenantId>/).
832
+ "brain-api-keys",
979
833
  # One-shot tenant provisioning: mints a tkm_ key + stores in Secrets
980
834
  # Manager at thinkwork/<stage>/mcp/<tenantId>/admin-ops + upserts
981
835
  # tenant_mcp_servers. SM IAM is already granted on thinkwork/* by
@@ -990,6 +844,16 @@ resource "aws_lambda_function" "handler" {
990
844
  # Daily sweeper: auto-rejects MCP servers pending > 30 days. Triggered
991
845
  # by EventBridge schedule (mcp-approval-sweeper-daily).
992
846
  "mcp-approval-sweeper",
847
+ # Daily sweeper: cancels computer_approval inbox items past expires_at
848
+ # (or older than the TTL, for rows predating it). Keeps the approvals
849
+ # queue a list of live decisions instead of a months-deep graveyard.
850
+ "inbox-approval-sweeper",
851
+ # Hourly retention sweeper for auth_subscription_tickets. Tickets are
852
+ # single-use and expire 60s after mint but were never deleted; the
853
+ # table reached 551k rows / 306 MB (idx_scan=0) on a customer stage.
854
+ # Batched ctid DELETEs, 1h grace past expires_at. Triggered by
855
+ # EventBridge (aws_scheduler_schedule.auth_ticket_sweeper).
856
+ "auth-ticket-sweeper",
993
857
  # Finance pilot U2 — thread-attachment upload (presign + finalize).
994
858
  # presign issues a 5-min PUT URL the end-user client uses to push
995
859
  # Excel/CSV bytes directly to S3; finalize sniffs magic bytes, scans
@@ -999,6 +863,11 @@ resource "aws_lambda_function" "handler" {
999
863
  # threads.tenant_id lookup. Needs WORKSPACE_BUCKET env for S3.
1000
864
  "thread-attachments-presign",
1001
865
  "thread-attachments-finalize",
866
+ # Runtime-generated file registration (execute_code output_files).
867
+ # Bearer API_AUTH_SECRET + x-tenant-id; the Pi runtime uploads the
868
+ # sandbox file to the same staging prefix and registers it here with
869
+ # the same magic-byte + OOXML validation as finalize.
870
+ "thread-attachments-register",
1002
871
  # U9-remainder of finance pilot — tenant-pinned download endpoint.
1003
872
  # GET /api/threads/{tid}/attachments/{aid}/download returns a 302
1004
873
  # to a 5-minute presigned S3 GET URL with ResponseContentDisposition:
@@ -1013,14 +882,10 @@ resource "aws_lambda_function" "handler" {
1013
882
  # POSTs one row per agent-session-start. Shared
1014
883
  # API_AUTH_SECRET bearer (runtime→API; no tenant OAuth).
1015
884
  "manifest-log",
1016
- # Governed capability runtime control-plane service (THINK-280 U2).
1017
- # NO HTTP route: the Pi container invokes it directly (RequestResponse)
1018
- # via lambda:InvokeFunction over the approved in-account path — public
1019
- # execute-api is not reliable from Pi's private VPC. Identity comes
1020
- # exclusively from the Ed25519-signed capability caller context (the
1021
- # CAPABILITY_SIGNING_PUBLIC_KEY in the shared runtime-config document
1022
- # verifies it); plaintext payload fields are never trusted.
1023
- "capability-control-service",
885
+ # Pi tool-execution ledger write endpoint (THINK-324 C17). The runtime
886
+ # POSTs paired started/terminal evidence rows per tool call. Shared
887
+ # API_AUTH_SECRET bearer (runtime→API; no tenant OAuth).
888
+ "tool-executions",
1024
889
  # SI-7 catalog-list read endpoint (plan §U15 pt 3/3). The runtime
1025
890
  # fetches the allowed builtin-tool slug set once per
1026
891
  # session-start + feature-flag-gated enforcement filter drops
@@ -1060,12 +925,44 @@ resource "aws_lambda_function" "handler" {
1060
925
  # new standalone resource (see U8b plan operator-step section).
1061
926
  ]), toset(local.optional_integration_handler_names)) : toset([])
1062
927
 
928
+ # THINK-1082: one vpc_config per function, resolved here so the resource
929
+ # block stays a single dynamic block. Three shapes:
930
+ # - identity-graph-projector with Neptune wired: the Neptune subnets (the
931
+ # projector must reach the Neptune cluster), security groups = the
932
+ # Neptune client SGs UNION the api-lambda SGs when lambdas_in_vpc is on
933
+ # (the projector also opens DATABASE_URL, so it needs the DB grant too).
934
+ # - any handler when lambdas_in_vpc: the VPC subnets + api-lambda SGs.
935
+ # - otherwise null: no vpc_config, byte-identical to today.
936
+ handler_vpc = {
937
+ for name in local.handler_names : name => (
938
+ name == "identity-graph-projector" && local.neptune_vpc_enabled ? {
939
+ subnet_ids = var.neptune_subnet_ids
940
+ security_group_ids = distinct(concat(
941
+ var.neptune_security_group_ids,
942
+ var.lambdas_in_vpc ? var.vpc_security_group_ids : [],
943
+ ))
944
+ } : var.lambdas_in_vpc ? {
945
+ subnet_ids = var.vpc_subnet_ids
946
+ security_group_ids = var.vpc_security_group_ids
947
+ } : null
948
+ )
949
+ }
950
+ }
951
+
952
+ resource "aws_lambda_function" "handler" {
953
+ for_each = local.handler_names
954
+
1063
955
  function_name = "thinkwork-${var.stage}-api-${each.key}"
956
+ # THINK-583 U3: publish a numbered version for the two chat-critical
957
+ # handlers only — provisioned concurrency requires a published version
958
+ # behind the `live` alias (resources below). Scoped so the other ~90
959
+ # handlers don't accumulate a version per apply.
960
+ publish = contains(local.provisioned_concurrency_handlers, each.key)
1064
961
  # S2 (THINK-193 U2): memory-retraction-drainer runs under a DEDICATED role
1065
962
  # so its destructive evidence-snapshot S3 capability (version enumeration +
1066
963
  # bulk version deletion) never leaks into the 90+ handlers on the shared
1067
964
  # role. Everything else keeps the shared role.
1068
- role = each.key == "memory-retraction-drainer" ? aws_iam_role.memory_retraction_drainer.arn : each.key == "turn-assertion-mint" ? aws_iam_role.turn_assertion_mint[0].arn : each.key == "agentcore-proof-oauth-provider" ? aws_iam_role.agentcore_proof_provider[0].arn : each.key == "agentcore-identity-boundary-target" ? aws_iam_role.agentcore_proof_boundary[0].arn : aws_iam_role.lambda.arn
965
+ role = each.key == "memory-retraction-drainer" ? aws_iam_role.memory_retraction_drainer.arn : aws_iam_role.lambda.arn
1069
966
  handler = "index.handler"
1070
967
  runtime = local.runtime
1071
968
  # Parameters and Secrets extension: container-local cache for the SSM
@@ -1082,9 +979,6 @@ resource "aws_lambda_function" "handler" {
1082
979
  # eval-runner walks every test case sequentially, invoking an agent +
1083
980
  # waiting up to 2 min for spans to propagate per test, so a 10-test run
1084
981
  # can easily exceed the 30 s default. 900 s covers ~5-15 min sweeps.
1085
- # wiki-bootstrap-import runs a full Hindsight ingest for ~3,000 records;
1086
- # the LLM-backed retain path makes it the longest-running Lambda in the
1087
- # set. 900 s is Lambda's per-invocation max and matches eval-runner's ceiling.
1088
982
  # routine-task-python wraps a 300s sandbox session and needs headroom
1089
983
  # for the Start/Invoke/Stop/S3-offload round trip; 360s leaves ~60s
1090
984
  # for AWS-call setup and offload after the sandbox's own ceiling.
@@ -1092,11 +986,16 @@ resource "aws_lambda_function" "handler" {
1092
986
  # validates the agent, builds the AgentCore invoke payload, dispatches
1093
987
  # Event-mode, and returns. Setup is ~5s in practice; 60s gives 12×
1094
988
  # headroom for transient slowness.
1095
- # harness-runner holds the Lambda open for the full Harness turn (the
1096
989
  # reference QBR run was ~2 min; 900s is the ceiling — a longer run is a
1097
990
  # legitimate trial limitation, recorded, not engineered around).
1098
- timeout = each.key == "harness-runner" ? 900 : each.key == "wakeup-processor" ? 300 : each.key == "chat-agent-invoke" ? 60 : each.key == "chat-agent-finalize" ? 60 : each.key == "workspace-event-dispatcher" ? 60 : each.key == "eval-runner" ? 900 : each.key == "eval-worker" ? 240 : each.key == "wiki-compile" ? 480 : each.key == "knowledge-graph-observations-ingest" ? 480 : each.key == "requester-memory-dreaming" ? 300 : each.key == "ontology-scan" ? 300 : each.key == "ontology-reprocess" ? 300 : each.key == "wiki-lint" ? 300 : each.key == "wiki-export" ? 600 : each.key == "okf-materialize" ? 600 : each.key == "okf-efs-refresh" ? 600 : each.key == "wiki-bootstrap-import" ? 900 : each.key == "folder-bundle-import" ? 300 : each.key == "routine-task-python" ? 360 : each.key == "routine-exec-git" ? 360 : each.key == "job-trigger" ? 600 : each.key == "model-converse" ? 60 : each.key == "memory-retain" ? 300 : each.key == "brain-dream-state" ? 900 : each.key == "memory-stage-worker" ? 900 : each.key == "memory-stage-sweeper" ? 120 : each.key == "memory-retraction-drainer" ? 300 : each.key == "canvas-refresh" ? 120 : each.key == "document-conformance-judge" ? 300 : each.key == "workflow-step-dispatch" ? 600 : each.key == "workflow-execution-callback" ? 60 : each.key == "workflow-resume" ? 60 : 30
1099
- memory_size = each.key == "harness-runner" ? 512 : 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 == "knowledge-graph-observations-ingest" ? 1024 : each.key == "requester-memory-dreaming" ? 512 : each.key == "ontology-scan" ? 512 : each.key == "wiki-export" ? 1024 : each.key == "okf-materialize" ? 1024 : each.key == "okf-efs-refresh" ? 1024 : each.key == "wiki-bootstrap-import" ? 1024 : each.key == "folder-bundle-import" ? 1024 : 256
991
+ timeout = each.key == "wakeup-processor" ? 300 : each.key == "chat-agent-invoke" ? 60 : each.key == "agentcore-runtime-dispatch" ? 900 : each.key == "agentcore-dispatch-dlq-redrive" ? 60 : each.key == "chat-agent-finalize" ? 60 : each.key == "workspace-event-dispatcher" ? 60 : each.key == "eval-runner" ? 900 : each.key == "eval-worker" ? 240 : each.key == "requester-memory-dreaming" ? 300 : each.key == "identity-match" ? 300 : each.key == "identity-graph-projector" ? 900 : each.key == "folder-bundle-import" ? 300 : each.key == "routine-task-python" ? 360 : each.key == "routine-exec-git" ? 360 : each.key == "job-trigger" ? 600 : each.key == "model-converse" ? 60 : each.key == "memory-retain" ? 300 : each.key == "memory-stage-worker" ? 900 : each.key == "memory-stage-sweeper" ? 120 : each.key == "auth-ticket-sweeper" ? 120 : each.key == "memory-retraction-drainer" ? 300 : each.key == "canvas-refresh" ? 120 : each.key == "document-conformance-judge" ? 300 : each.key == "workflow-step-dispatch" ? 600 : each.key == "workflow-execution-callback" ? 60 : each.key == "workflow-resume" ? 60 : 30
992
+ # THINK-583 U2: chat-agent-invoke and workspace-renderer get a full vCPU
993
+ # (1769 MB). At 256 MB (~1/7 vCPU) the parallelized setup awaits starved on
994
+ # CPU — per-leg timing showed three independent {1 DB read + 1 S3/Secrets
995
+ # read} legs each taking ~1.5 s in parallel while sequential stage-1 reads
996
+ # were fast. Memory is priced per GB-s, but wall-clock drops far more than
997
+ # 7x cost rises on these short, provisioned-concurrency-warmed functions.
998
+ 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 == "requester-memory-dreaming" ? 512 : each.key == "identity-match" ? 512 : each.key == "identity-graph-projector" ? min(8192, var.lambda_max_memory_mb) : each.key == "folder-bundle-import" ? 1024 : each.key == "chat-agent-invoke" ? 1769 : each.key == "workspace-renderer" ? 1769 : each.key == "agentcore-runtime-dispatch" ? 512 : 256
1100
999
 
1101
1000
  filename = local.use_local_zips ? "${var.lambda_zips_dir}/${each.key}.zip" : null
1102
1001
  source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/${each.key}.zip") : null
@@ -1114,64 +1013,32 @@ resource "aws_lambda_function" "handler" {
1114
1013
  # document-conformance-judge is also a single-writer: direct
1115
1014
  # process-and-complete with no in-flight claim status depends on never
1116
1015
  # having two sweepers race the same pending rows (THINK-189 KTD4).
1117
- # analyst-query-broker is capped low: each concurrent execution holds a
1118
- # dedicated analyst_reader Postgres connection and runs model-authored
1119
- # SQL — the cap bounds both connection pressure and the blast radius of
1120
- # a runaway delegation (THINK-228 U3).
1121
- # analyst-connection-reconciler is capped at 1: the probe holds one
1122
- # analyst_reader connection and overlapping probes are pointless (the
1123
- # cluster-global reader has one grant surface / one live schema).
1124
- reserved_concurrent_executions = each.key == "compliance-outbox-drainer" ? 1 : each.key == "document-conformance-judge" ? 1 : each.key == "eval-worker" ? 40 : each.key == "analyst-query-broker" ? 4 : each.key == "analyst-connection-reconciler" ? 1 : -1
1016
+ reserved_concurrent_executions = each.key == "compliance-outbox-drainer" ? 1 : each.key == "document-conformance-judge" ? 1 : each.key == "eval-worker" ? 40 : -1
1125
1017
 
1126
1018
  environment {
1127
1019
  variables = merge(
1128
- each.key == "turn-assertion-mint" ? local.turn_assertion_mint_env : contains([
1129
- "agentcore-proof-oauth-provider",
1130
- "agentcore-identity-boundary-target",
1131
- ], each.key) ? local.agentcore_proof_boundary_env : local.common_env,
1020
+ local.common_env,
1132
1021
  { FUNCTION_NAME = each.key },
1133
1022
  lookup(local.handler_extra_env, each.key, {}),
1134
1023
  )
1135
1024
  }
1136
1025
 
1026
+ # Company Brain U5 + THINK-1082: at most one vpc_config per function. The
1027
+ # placement (Neptune projector, whole-module VPC lane, or none) is resolved
1028
+ # in local.handler_vpc above; absent entirely when that entry is null.
1137
1029
  dynamic "vpc_config" {
1138
- for_each = each.key == "okf-efs-refresh" && local.okf_efs_vpc_enabled ? [1] : []
1139
-
1140
- content {
1141
- subnet_ids = var.okf_efs_subnet_ids
1142
- security_group_ids = var.okf_efs_security_group_ids
1143
- }
1144
- }
1145
-
1146
- # Analyst data-path handlers get a stable NAT egress IP when
1147
- # analyst_egress_subnet_ids is set — external Postgres sources behind an IP
1148
- # allowlist admit that one EIP. Disjoint from the okf-efs-refresh block
1149
- # above (a function takes at most one vpc_config).
1150
- dynamic "vpc_config" {
1151
- for_each = contains(local.analyst_vpc_handlers, each.key) && local.analyst_vpc_enabled ? [1] : []
1152
-
1153
- content {
1154
- subnet_ids = var.analyst_egress_subnet_ids
1155
- security_group_ids = var.analyst_egress_security_group_ids
1156
- }
1157
- }
1158
-
1159
- dynamic "file_system_config" {
1160
- for_each = each.key == "okf-efs-refresh" && local.okf_efs_vpc_enabled ? [1] : []
1030
+ for_each = local.handler_vpc[each.key] == null ? [] : [local.handler_vpc[each.key]]
1161
1031
 
1162
1032
  content {
1163
- arn = var.okf_efs_refresh_access_point_arn
1164
- local_mount_path = var.okf_efs_mount_path
1033
+ subnet_ids = vpc_config.value.subnet_ids
1034
+ security_group_ids = vpc_config.value.security_group_ids
1165
1035
  }
1166
1036
  }
1167
1037
 
1168
1038
  lifecycle {
1169
1039
  precondition {
1170
- condition = each.key != "okf-efs-refresh" || var.okf_efs_refresh_access_point_arn == "" || (
1171
- length(var.okf_efs_mount_target_ids) == length(var.okf_efs_subnet_ids) &&
1172
- alltrue([for id in var.okf_efs_mount_target_ids : id != ""])
1173
- )
1174
- error_message = "okf-efs-refresh requires an available EFS mount target dependency for every configured subnet."
1040
+ condition = local.lambda_vpc_inputs_complete
1041
+ error_message = "lambdas_in_vpc requires non-empty vpc_subnet_ids and vpc_security_group_ids."
1175
1042
  }
1176
1043
  }
1177
1044
 
@@ -1182,31 +1049,59 @@ resource "aws_lambda_function" "handler" {
1182
1049
  }
1183
1050
 
1184
1051
  # ---------------------------------------------------------------------------
1185
- # wiki-compile async retry config + DLQ
1052
+ # Warm sessions (THINK-583 U3): published version + `live` alias +
1053
+ # provisioned concurrency for chat-agent-invoke and workspace-renderer
1186
1054
  # ---------------------------------------------------------------------------
1055
+ # KTD5: these two zip Lambdas ONLY — never the Pi Lambda (its warmth is
1056
+ # superseded by AgentCore session reuse, and deploy.yml publishes Pi code
1057
+ # to $LATEST out-of-band).
1187
1058
  #
1188
- # AWS Lambda's default async invoke retries the function 2 times with a
1189
- # 1-minute delay before sending failures to a DLQ (or dropping). For
1190
- # wiki-compile, retries duplicate Bedrock cost AND can produce duplicate
1191
- # user-visible threads + workspace_runs (the brain-enrichment draft path
1192
- # in particularsee plan 2026-05-01-002 U5/U6 and
1193
- # docs/solutions/architecture-patterns/async-retry-idempotency-lessons).
1194
- #
1195
- # Pin retries to 0 and route failures to a dedicated DLQ. The runner's
1196
- # job-status short-circuit (running/succeeded/failed/skipped) is the
1197
- # in-process protection against duplicate writebacks; this is the
1198
- # infrastructure-level belt-and-suspenders.
1059
+ # Provisioned concurrency only serves invokes addressed to the alias, so
1060
+ # EVERY invoker resolves `<fn>:live` (graphql/utils.ts, mobile-turns/
1061
+ # managed-dispatch.ts, and the workspaceRendererFunctionName helpers in
1062
+ # chat-agent-invoke.ts / wakeup-processor.ts). The alias exists even when
1063
+ # the concurrency count is 0 an alias with no PC config serves normally
1064
+ # from $LATEST-equivalent cold starts — so disabled stages keep working
1065
+ # unchanged and opt in purely by raising the count variable.
1199
1066
 
1200
- resource "aws_sqs_queue" "wiki_compile_dlq" {
1201
- count = local.deploy_lambda_handlers ? 1 : 0
1202
- name = "thinkwork-${var.stage}-wiki-compile-dlq"
1203
- message_retention_seconds = 1209600 # 14 days
1204
-
1205
- tags = {
1206
- Name = "thinkwork-${var.stage}-wiki-compile-dlq"
1067
+ locals {
1068
+ provisioned_concurrency_handlers = toset([
1069
+ "chat-agent-invoke",
1070
+ "workspace-renderer",
1071
+ ])
1072
+ provisioned_concurrency_counts = {
1073
+ "chat-agent-invoke" = var.chat_agent_invoke_provisioned_concurrency
1074
+ "workspace-renderer" = var.workspace_renderer_provisioned_concurrency
1207
1075
  }
1208
1076
  }
1209
1077
 
1078
+ resource "aws_lambda_alias" "live" {
1079
+ for_each = local.deploy_lambda_handlers ? local.provisioned_concurrency_handlers : toset([])
1080
+
1081
+ name = "live"
1082
+ description = "Warm-session alias (THINK-583 U3) — provisioned-concurrency target; all invokers alias-qualify."
1083
+ function_name = aws_lambda_function.handler[each.key].function_name
1084
+ function_version = aws_lambda_function.handler[each.key].version
1085
+ }
1086
+
1087
+ resource "aws_lambda_provisioned_concurrency_config" "live" {
1088
+ # aws_lambda_provisioned_concurrency_config rejects a count of 0, so a
1089
+ # disabled handler simply has no config resource (the alias remains).
1090
+ for_each = local.deploy_lambda_handlers ? {
1091
+ for name in local.provisioned_concurrency_handlers :
1092
+ name => local.provisioned_concurrency_counts[name]
1093
+ if local.provisioned_concurrency_counts[name] > 0
1094
+ } : {}
1095
+
1096
+ function_name = aws_lambda_function.handler[each.key].function_name
1097
+ qualifier = aws_lambda_alias.live[each.key].name
1098
+ provisioned_concurrent_executions = each.value
1099
+ }
1100
+
1101
+ # ---------------------------------------------------------------------------
1102
+ # Async retry configuration
1103
+ # ---------------------------------------------------------------------------
1104
+
1210
1105
  # chat-agent-invoke retry-0 (plan 2026-05-22-006 U3). After the
1211
1106
  # direct-callback finalize refactor, chat-agent-invoke is Event-mode-
1212
1107
  # dispatched by the GraphQL resolver and itself dispatches AgentCore
@@ -1225,80 +1120,41 @@ resource "aws_lambda_function_event_invoke_config" "chat_agent_invoke" {
1225
1120
  maximum_event_age_in_seconds = 3600
1226
1121
  }
1227
1122
 
1228
- # harness-runner retry-0 (THINK-311 U5, async-retry idempotency): an AWS
1229
- # async retry would re-execute a whole agent turn. Failures finalize the
1230
- # turn in-process (finalize-through-failure); a hard crash is reconciled
1231
- # by the stall monitor (timed_out + checkout release, no retry enqueue
1232
- # for harness turns). No DLQ destination for the same reason as
1233
- # chat-agent-invoke above: the shared role's inline policy budget is at
1234
- # the AWS cap, and the stall monitor is the operational surface.
1235
- resource "aws_lambda_function_event_invoke_config" "harness_runner" {
1236
- count = local.deploy_lambda_handlers ? 1 : 0
1237
- function_name = aws_lambda_function.handler["harness-runner"].function_name
1238
- maximum_retry_attempts = 0
1239
- maximum_event_age_in_seconds = 3600
1240
- }
1241
-
1242
- resource "aws_lambda_function_event_invoke_config" "wiki_compile" {
1123
+ # THINK-583 U3: the resolver now Event-invokes the `live` alias, and async
1124
+ # invoke config is per-qualifier without this duplicate on the alias the
1125
+ # alias path would regress to AWS's default 2 retries and the 5-min stall
1126
+ # cascade above could recur. Same retry-0 posture, pinned explicitly.
1127
+ resource "aws_lambda_function_event_invoke_config" "chat_agent_invoke_live" {
1243
1128
  count = local.deploy_lambda_handlers ? 1 : 0
1244
- function_name = aws_lambda_function.handler["wiki-compile"].function_name
1245
- maximum_retry_attempts = 0
1246
- maximum_event_age_in_seconds = 3600
1247
-
1248
- destination_config {
1249
- on_failure {
1250
- destination = aws_sqs_queue.wiki_compile_dlq[0].arn
1251
- }
1252
- }
1253
- }
1254
-
1255
- # Ontology suggestion scans are durable-job driven. Disable AWS async
1256
- # retries so duplicate scan invocations do not create duplicate review
1257
- # proposals; the scan job row is the retry/observability surface.
1258
- resource "aws_sqs_queue" "ontology_scan_dlq" {
1259
- count = local.deploy_lambda_handlers ? 1 : 0
1260
- name = "thinkwork-${var.stage}-ontology-scan-dlq"
1261
- message_retention_seconds = 1209600 # 14 days
1262
-
1263
- tags = {
1264
- Name = "thinkwork-${var.stage}-ontology-scan-dlq"
1265
- }
1266
- }
1267
-
1268
- resource "aws_lambda_function_event_invoke_config" "ontology_scan" {
1269
- count = local.deploy_lambda_handlers ? 1 : 0
1270
- function_name = aws_lambda_function.handler["ontology-scan"].function_name
1129
+ function_name = aws_lambda_function.handler["chat-agent-invoke"].function_name
1130
+ qualifier = aws_lambda_alias.live["chat-agent-invoke"].name
1271
1131
  maximum_retry_attempts = 0
1272
1132
  maximum_event_age_in_seconds = 3600
1273
-
1274
- destination_config {
1275
- on_failure {
1276
- destination = aws_sqs_queue.ontology_scan_dlq[0].arn
1277
- }
1278
- }
1279
1133
  }
1280
1134
 
1281
- # Ontology reprocess jobs are row-ledger driven and explicitly claim work.
1282
- # Disable AWS async retries to keep failure/retry state in ontology.reprocess_jobs.
1283
- resource "aws_sqs_queue" "ontology_reprocess_dlq" {
1135
+ # Identity match jobs (THINK-321 U7) are durable-job driven. Disable AWS
1136
+ # async retries so duplicate match invocations do not
1137
+ # double-write mappings/cases; identity.match_jobs is the retry/
1138
+ # observability surface.
1139
+ resource "aws_sqs_queue" "identity_match_dlq" {
1284
1140
  count = local.deploy_lambda_handlers ? 1 : 0
1285
- name = "thinkwork-${var.stage}-ontology-reprocess-dlq"
1141
+ name = "thinkwork-${var.stage}-identity-match-dlq"
1286
1142
  message_retention_seconds = 1209600 # 14 days
1287
1143
 
1288
1144
  tags = {
1289
- Name = "thinkwork-${var.stage}-ontology-reprocess-dlq"
1145
+ Name = "thinkwork-${var.stage}-identity-match-dlq"
1290
1146
  }
1291
1147
  }
1292
1148
 
1293
- resource "aws_lambda_function_event_invoke_config" "ontology_reprocess" {
1149
+ resource "aws_lambda_function_event_invoke_config" "identity_match" {
1294
1150
  count = local.deploy_lambda_handlers ? 1 : 0
1295
- function_name = aws_lambda_function.handler["ontology-reprocess"].function_name
1151
+ function_name = aws_lambda_function.handler["identity-match"].function_name
1296
1152
  maximum_retry_attempts = 0
1297
1153
  maximum_event_age_in_seconds = 3600
1298
1154
 
1299
1155
  destination_config {
1300
1156
  on_failure {
1301
- destination = aws_sqs_queue.ontology_reprocess_dlq[0].arn
1157
+ destination = aws_sqs_queue.identity_match_dlq[0].arn
1302
1158
  }
1303
1159
  }
1304
1160
  }
@@ -1322,7 +1178,7 @@ resource "aws_lambda_function_event_invoke_config" "routine_approval_callback" {
1322
1178
  # memory-retain after every chat turn. AWS Lambda's default async-retry
1323
1179
  # policy is 2 attempts; without overriding it, a transient failure on the
1324
1180
  # canonical-transcript fetch or adapter write retries the entire writeback
1325
- # and can multi-write the same per-turn document into Hindsight. The
1181
+ # and can multi-write the same per-turn document into memory. The
1326
1182
  # longest-suffix-prefix merge in memory-retain.ts dedupes content but the
1327
1183
  # retain-cost path (Bedrock tokens charged in adapter.retainConversation)
1328
1184
  # is NOT idempotent — retries multiply LLM cost. Per
@@ -1632,21 +1488,6 @@ locals {
1632
1488
  # Agent actions (start/stop/heartbeat/budget)
1633
1489
  "ANY /api/agent-actions/{proxy+}" = "agent-actions"
1634
1490
 
1635
- # Desktop-local Pi tombstones. Specific routes before broad REST handlers;
1636
- # OPTIONS is handled inside the Lambda before auth.
1637
- "POST /api/desktop/runtime-session" = "desktop-runtime-session"
1638
- "OPTIONS /api/desktop/runtime-session" = "desktop-runtime-session"
1639
- "POST /api/desktop/workspace-prewarm" = "desktop-workspace-prewarm"
1640
- "OPTIONS /api/desktop/workspace-prewarm" = "desktop-workspace-prewarm"
1641
- "POST /api/desktop/managed-delegation" = "managed-delegation"
1642
- "OPTIONS /api/desktop/managed-delegation" = "managed-delegation"
1643
- "POST /api/desktop/eval-runs" = "desktop-eval-runs"
1644
- "OPTIONS /api/desktop/eval-runs" = "desktop-eval-runs"
1645
- "POST /api/desktop/eval-runs/{runId}/sessions" = "desktop-eval-runs"
1646
- "OPTIONS /api/desktop/eval-runs/{runId}/sessions" = "desktop-eval-runs"
1647
- "POST /api/desktop/eval-runs/{runId}/results" = "desktop-eval-runs"
1648
- "OPTIONS /api/desktop/eval-runs/{runId}/results" = "desktop-eval-runs"
1649
-
1650
1491
  # Mobile agent harness model proxy (cloud Bedrock Converse). OPTIONS is
1651
1492
  # handled inside the Lambda before auth.
1652
1493
  "POST /api/model/converse" = "model-converse"
@@ -1719,34 +1560,6 @@ locals {
1719
1560
  # under /agentcore so the existing user-memory OAuth issuer is unchanged.
1720
1561
  "GET /agentcore/.well-known/openid-configuration" = "mcp-oauth"
1721
1562
  "GET /agentcore/oauth/jwks" = "mcp-oauth"
1722
- # THINK-316 U1 proof-only provider and downstream HTTPS target. Both
1723
- # handlers validate their own credentials; the route filter removes all
1724
- # five when enable_agentcore_multiplayer_proof is false.
1725
- "GET /agentcore-proof/oauth/.well-known/openid-configuration" = "agentcore-proof-oauth-provider"
1726
- "GET /agentcore-proof/oauth/authorize" = "agentcore-proof-oauth-provider"
1727
- "POST /agentcore-proof/oauth/token" = "agentcore-proof-oauth-provider"
1728
- "GET /agentcore-proof/target/owner" = "agentcore-identity-boundary-target"
1729
- "GET /agentcore-proof/target/mixed" = "agentcore-identity-boundary-target"
1730
- "POST /agentcore/capabilities/mcp/tools/list" = "harness-capability-mcp"
1731
- "POST /agentcore/capabilities/mcp/tools/call" = "harness-capability-mcp"
1732
- "POST /agentcore/capabilities/sandbox/execute" = "harness-code-interpreter-target"
1733
- "POST /agentcore/capabilities/web/search" = "harness-builtin-tools-target"
1734
- "POST /agentcore/capabilities/web/extract" = "harness-builtin-tools-target"
1735
- "POST /agentcore/capabilities/brain/query" = "harness-platform-tools-target"
1736
- "POST /agentcore/capabilities/workspace/skills/list" = "harness-platform-tools-target"
1737
- "POST /agentcore/capabilities/workspace/skills/load" = "harness-platform-tools-target"
1738
- "POST /agentcore/capabilities/message/attachments/list" = "harness-platform-tools-target"
1739
- "POST /agentcore/capabilities/message/attachments/read" = "harness-platform-tools-target"
1740
- "POST /agentcore/capabilities/user/questions/ask" = "harness-platform-tools-target"
1741
- "POST /agentcore/capabilities/email/send" = "harness-platform-tools-target"
1742
- "POST /mcp/oauth/register" = "mcp-oauth"
1743
- "GET /mcp/oauth/authorize" = "mcp-oauth"
1744
- "GET /mcp/oauth/callback" = "mcp-oauth"
1745
- "POST /mcp/oauth/token" = "mcp-oauth"
1746
- "POST /mcp/oauth/revoke" = "mcp-oauth"
1747
- "ANY /mcp/user-memory" = "mcp-user-memory"
1748
- "ANY /mcp/context-engine" = "mcp-context-engine"
1749
- "ANY /mcp/open-engine" = "mcp-open-engine"
1750
1563
  # THINK-280 U8: scoped external capability search facade. The handler is
1751
1564
  # inert unless CAPABILITY_EXTERNAL_SEARCH_ENABLED (gated on
1752
1565
  # enable_capability_broker) is on — routing it while disabled returns an
@@ -1861,9 +1674,6 @@ locals {
1861
1674
  # Workspace files
1862
1675
  "ANY /api/workspaces/{proxy+}" = "workspace-files"
1863
1676
 
1864
- # Knowledge bases
1865
- "ANY /api/knowledge-bases/{proxy+}" = "knowledge-base-files"
1866
-
1867
1677
  # Email
1868
1678
  "POST /api/email/send" = "email-send"
1869
1679
  "POST /api/email/provider-webhook/{providerInstallId}" = "email-provider-webhook"
@@ -1885,7 +1695,6 @@ locals {
1885
1695
  "POST /msteams/account-link/complete" = "msteams-account-link-complete"
1886
1696
 
1887
1697
  # Memory
1888
- "ANY /api/memory/{proxy+}" = "memory"
1889
1698
 
1890
1699
  # Artifacts
1891
1700
  "POST /api/artifacts/{proxy+}" = "artifact-deliver"
@@ -1951,20 +1760,6 @@ locals {
1951
1760
  # retained as a break-glass superuser path for bootstrap/debug.
1952
1761
  "POST /mcp/admin" = "admin-ops-mcp"
1953
1762
 
1954
- # Analyst query broker — first-party MCP server exposing query
1955
- # (THINK-228 U3). Callers present the tenant-wide broker service
1956
- # credential as Bearer; SQL executes as the hardened analyst_reader
1957
- # role. The seeded postgres-dev connector row points at this route.
1958
- "POST /mcp/analyst" = "analyst-query-broker"
1959
-
1960
- # Sourced analyst broker route (THINK-239) — the same handler serves a
1961
- # registered EXTERNAL Postgres source at /mcp/analyst/<slug>. The broker
1962
- # requires a signed caller context whose sourceClaims.slug matches the
1963
- # path (the legacy bearer is never accepted here) and connects using the
1964
- # per-source reader credential (thinkwork/<stage>/analyst/*, covered by
1965
- # the shared secretsmanager:GetSecretValue on thinkwork/*).
1966
- "POST /mcp/analyst/{sourceSlug}" = "analyst-query-broker"
1967
-
1968
1763
  # MCP admin key management — per-tenant Bearer token CRUD. Tokens
1969
1764
  # are shown ONCE at creation (POST returns raw value); server stores
1970
1765
  # sha256 hash only. These specific routes take precedence over the
@@ -1974,6 +1769,15 @@ locals {
1974
1769
  "GET /api/tenants/{tenantId}/mcp-admin-keys" = "mcp-admin-keys"
1975
1770
  "DELETE /api/tenants/{tenantId}/mcp-admin-keys/{keyId}" = "mcp-admin-keys"
1976
1771
 
1772
+ "POST /api/tenants/{tenantId}/brain-api-keys" = "brain-api-keys"
1773
+ "GET /api/tenants/{tenantId}/brain-api-keys" = "brain-api-keys"
1774
+ "OPTIONS /api/tenants/{tenantId}/brain-api-keys" = "brain-api-keys"
1775
+ "DELETE /api/tenants/{tenantId}/brain-api-keys/{keyId}" = "brain-api-keys"
1776
+ # Per-key grants edit (twin-mcp-keys/v2): securityGroups +
1777
+ # kbCollections, republishing the manifest on change.
1778
+ "PATCH /api/tenants/{tenantId}/brain-api-keys/{keyId}" = "brain-api-keys"
1779
+ "OPTIONS /api/tenants/{tenantId}/brain-api-keys/{keyId}" = "brain-api-keys"
1780
+
1977
1781
  # One-shot tenant provisioning for the admin-ops MCP. Mints a fresh
1978
1782
  # tkm_ key + stores it in Secrets Manager at
1979
1783
  # thinkwork/<stage>/mcp/<tenantId>/admin-ops + upserts the
@@ -1981,6 +1785,13 @@ locals {
1981
1785
  # any agent that gets it assigned via agent_mcp_servers.
1982
1786
  "POST /api/tenants/{tenantId}/mcp-admin-provision" = "mcp-admin-provision"
1983
1787
 
1788
+ # Digital Twin MCP provisioning (THINK-333 U4): mints the tkt_ key,
1789
+ # writes the secret, upserts the approved digital-twin connector row
1790
+ # (url_hash pinned), and materializes connectors/digital-twin/ into
1791
+ # every agent workspace. Re-run = rotate.
1792
+ "POST /api/tenants/{tenantId}/mcp-twin-provision" = "mcp-twin-provision"
1793
+ "OPTIONS /api/tenants/{tenantId}/mcp-twin-provision" = "mcp-twin-provision"
1794
+
1984
1795
  # MCP server admin approval (plan §U11, SI-5). Externally-sourced MCP
1985
1796
  # servers land with status='pending'; these routes flip them to
1986
1797
  # approved/rejected. Cognito JWT only (mcp-approval handler rejects
@@ -1998,6 +1809,10 @@ locals {
1998
1809
  "POST /api/threads/{threadId}/attachments/finalize" = "thread-attachments-finalize"
1999
1810
  "OPTIONS /api/threads/{threadId}/attachments/finalize" = "thread-attachments-finalize"
2000
1811
 
1812
+ # Runtime-generated attachments (execute_code output_files). Shared
1813
+ # API_AUTH_SECRET; no browser callers, so no OPTIONS route.
1814
+ "POST /api/threads/{threadId}/attachments/register" = "thread-attachments-register"
1815
+
2001
1816
  # U9-remainder of finance pilot — tenant-pinned download endpoint.
2002
1817
  "GET /api/threads/{threadId}/attachments/{attachmentId}/download" = "thread-attachment-download"
2003
1818
  "OPTIONS /api/threads/{threadId}/attachments/{attachmentId}/download" = "thread-attachment-download"
@@ -2012,13 +1827,19 @@ locals {
2012
1827
  "POST /api/runtime/manifests" = "manifest-log"
2013
1828
  "OPTIONS /api/runtime/manifests" = "manifest-log"
2014
1829
 
1830
+ # Pi tool-execution ledger (THINK-324 C17). The runtime POSTs paired
1831
+ # started/terminal evidence rows per tool call. Shared API_AUTH_SECRET;
1832
+ # no tenant OAuth.
1833
+ "POST /api/runtime/tool-executions" = "tool-executions"
1834
+ "OPTIONS /api/runtime/tool-executions" = "tool-executions"
1835
+
2015
1836
  # SI-7 catalog-list read (plan §U15 pt 3/3). The runtime fetches
2016
1837
  # the allowed slug set once per session-start. Shared API_AUTH_SECRET.
2017
1838
  "GET /api/runtime/capability-catalog" = "capability-catalog-list"
2018
1839
  "OPTIONS /api/runtime/capability-catalog" = "capability-catalog-list"
2019
1840
  } : route_key => handler_name
2020
1841
  if !contains(local.optional_integration_handler_names, handler_name) && (
2021
- var.enable_agentcore_multiplayer_proof || !startswith(route_key, "GET /agentcore/")
1842
+ !startswith(route_key, "GET /agentcore/")
2022
1843
  )
2023
1844
  } : {}
2024
1845
  }
@@ -2177,42 +1998,6 @@ resource "aws_scheduler_schedule" "webhook_deliveries_cleanup" {
2177
1998
  # Requester memory dreaming — broad per-user memory compaction/reflection sweep
2178
1999
  # ---------------------------------------------------------------------------
2179
2000
 
2180
- # ---------------------------------------------------------------------------
2181
- # Brain dream state — per-bank Hindsight consolidation with audit ledger
2182
- # (THINK-133 U4). Retries are the ledger's job (staged plan -> atomic apply
2183
- # -> applied markers; unfinished runs resume on the next tick), so Lambda
2184
- # async retries stay at 0, mirroring memory-retain.
2185
- # ---------------------------------------------------------------------------
2186
-
2187
- resource "aws_lambda_function_event_invoke_config" "brain_dream_state" {
2188
- count = local.deploy_lambda_handlers ? 1 : 0
2189
- function_name = aws_lambda_function.handler["brain-dream-state"].function_name
2190
- maximum_retry_attempts = 0
2191
- maximum_event_age_in_seconds = 3600
2192
- }
2193
-
2194
- resource "aws_scheduler_schedule" "brain_dream_state" {
2195
- count = local.deploy_lambda_handlers ? 1 : 0
2196
-
2197
- name = "thinkwork-${var.stage}-brain-dream-state"
2198
- group_name = "default"
2199
- schedule_expression = var.brain_dream_state_schedule_expression
2200
- state = var.brain_dream_state_enabled ? "ENABLED" : "DISABLED"
2201
-
2202
- flexible_time_window {
2203
- mode = "OFF"
2204
- }
2205
-
2206
- target {
2207
- arn = aws_lambda_function.handler["brain-dream-state"].arn
2208
- role_arn = aws_iam_role.scheduler.arn
2209
-
2210
- retry_policy {
2211
- maximum_retry_attempts = 0
2212
- }
2213
- }
2214
- }
2215
-
2216
2001
  resource "aws_scheduler_schedule" "requester_memory_dreaming" {
2217
2002
  count = local.deploy_lambda_handlers ? 1 : 0
2218
2003
 
@@ -2275,20 +2060,12 @@ resource "aws_scheduler_schedule" "mcp_approval_sweeper" {
2275
2060
  }
2276
2061
  }
2277
2062
 
2278
- # ---------------------------------------------------------------------------
2279
- # skill_runs reconciler — transitions stuck-running rows to failed every 5 min.
2280
- # Guards against agentcore Lambda crashes / OOMs that drop the
2281
- # /api/skills/complete writeback and leave the row at 'running' forever,
2282
- # which in turn blocks the dedup partial unique index from letting retries
2283
- # through.
2284
- # ---------------------------------------------------------------------------
2285
-
2286
- resource "aws_scheduler_schedule" "skill_runs_reconciler" {
2063
+ resource "aws_scheduler_schedule" "inbox_approval_sweeper" {
2287
2064
  count = local.deploy_lambda_handlers ? 1 : 0
2288
2065
 
2289
- name = "thinkwork-${var.stage}-skill-runs-reconciler"
2066
+ name = "thinkwork-${var.stage}-inbox-approval-sweeper"
2290
2067
  group_name = "default"
2291
- schedule_expression = "rate(5 minutes)"
2068
+ schedule_expression = "cron(30 4 * * ? *)" # daily at 04:30 UTC (offset from mcp-approval-sweeper)
2292
2069
  state = "ENABLED"
2293
2070
 
2294
2071
  flexible_time_window {
@@ -2296,53 +2073,50 @@ resource "aws_scheduler_schedule" "skill_runs_reconciler" {
2296
2073
  }
2297
2074
 
2298
2075
  target {
2299
- arn = aws_lambda_function.handler["skill-runs-reconciler"].arn
2076
+ arn = aws_lambda_function.handler["inbox-approval-sweeper"].arn
2300
2077
  role_arn = aws_iam_role.scheduler.arn
2301
2078
  }
2302
2079
  }
2303
2080
 
2304
- # ---------------------------------------------------------------------------
2305
- # analyst_connection_reconciler probes the analyst_reader connection every
2306
- # 30 min (reachability, IAM auth, SELECT-grant introspection, zero-write
2307
- # assertion, schema-drift hash; all read-only) and stamps the verdict onto
2308
- # tenant_mcp_servers.runtime_metadata.analyst_probe. Dispatch withholds a
2309
- # failing/stale connection loudly (THINK-229 U5, R7/R8, KTD8).
2310
- #
2311
- # retry-0 + DLQ (project_async_retry_idempotency_lessons): the probe is
2312
- # cheap and idempotent the next 30-minute tick IS the retry, so stacking
2313
- # scheduler retries only adds redundant connections. A failure lands in the
2314
- # DLQ for operator visibility instead.
2315
- # ---------------------------------------------------------------------------
2081
+ # auth_subscription_tickets retention. Unlike the approval sweepers this is
2082
+ # a volume problem, not a queue-hygiene one: ~30 tickets/min are minted on
2083
+ # every connect + subscription registration and nothing deleted them
2084
+ # (551k rows / 306 MB and +24 MB/day on a customer stage, idx_scan=0).
2085
+ # Hourly rather than daily so each tick's backlog stays small enough that a
2086
+ # single batch usually drains it; the handler's own 45s budget bounds the
2087
+ # first few catch-up runs on a stage with an existing backlog.
2088
+ resource "aws_scheduler_schedule" "auth_ticket_sweeper" {
2089
+ count = local.deploy_lambda_handlers ? 1 : 0
2316
2090
 
2317
- resource "aws_sqs_queue" "analyst_connection_reconciler_dlq" {
2318
- count = local.deploy_lambda_handlers ? 1 : 0
2319
- name = "thinkwork-${var.stage}-analyst-connection-reconciler-dlq"
2320
- message_retention_seconds = 1209600 # 14 days
2091
+ name = "thinkwork-${var.stage}-auth-ticket-sweeper"
2092
+ group_name = "default"
2093
+ schedule_expression = "rate(1 hour)"
2094
+ state = "ENABLED"
2321
2095
 
2322
- tags = {
2323
- Name = "thinkwork-${var.stage}-analyst-connection-reconciler-dlq"
2096
+ flexible_time_window {
2097
+ mode = "OFF"
2324
2098
  }
2325
- }
2326
2099
 
2327
- resource "aws_lambda_function_event_invoke_config" "analyst_connection_reconciler" {
2328
- count = local.deploy_lambda_handlers ? 1 : 0
2329
- function_name = aws_lambda_function.handler["analyst-connection-reconciler"].function_name
2330
- maximum_retry_attempts = 0
2331
- maximum_event_age_in_seconds = 3600
2332
-
2333
- destination_config {
2334
- on_failure {
2335
- destination = aws_sqs_queue.analyst_connection_reconciler_dlq[0].arn
2336
- }
2100
+ target {
2101
+ arn = aws_lambda_function.handler["auth-ticket-sweeper"].arn
2102
+ role_arn = aws_iam_role.scheduler.arn
2337
2103
  }
2338
2104
  }
2339
2105
 
2340
- resource "aws_scheduler_schedule" "analyst_connection_reconciler" {
2106
+ # ---------------------------------------------------------------------------
2107
+ # skill_runs reconciler — transitions stuck-running rows to failed every 5 min.
2108
+ # Guards against agentcore Lambda crashes / OOMs that drop the
2109
+ # /api/skills/complete writeback and leave the row at 'running' forever,
2110
+ # which in turn blocks the dedup partial unique index from letting retries
2111
+ # through.
2112
+ # ---------------------------------------------------------------------------
2113
+
2114
+ resource "aws_scheduler_schedule" "skill_runs_reconciler" {
2341
2115
  count = local.deploy_lambda_handlers ? 1 : 0
2342
2116
 
2343
- name = "thinkwork-${var.stage}-analyst-connection-reconciler"
2117
+ name = "thinkwork-${var.stage}-skill-runs-reconciler"
2344
2118
  group_name = "default"
2345
- schedule_expression = "rate(30 minutes)"
2119
+ schedule_expression = "rate(5 minutes)"
2346
2120
  state = "ENABLED"
2347
2121
 
2348
2122
  flexible_time_window {
@@ -2350,12 +2124,8 @@ resource "aws_scheduler_schedule" "analyst_connection_reconciler" {
2350
2124
  }
2351
2125
 
2352
2126
  target {
2353
- arn = aws_lambda_function.handler["analyst-connection-reconciler"].arn
2127
+ arn = aws_lambda_function.handler["skill-runs-reconciler"].arn
2354
2128
  role_arn = aws_iam_role.scheduler.arn
2355
-
2356
- retry_policy {
2357
- maximum_retry_attempts = 0
2358
- }
2359
2129
  }
2360
2130
  }
2361
2131
 
@@ -2721,160 +2491,39 @@ resource "aws_scheduler_schedule" "stall_monitor" {
2721
2491
  # Compounding Memory — nightly hygiene + export
2722
2492
  # ---------------------------------------------------------------------------
2723
2493
 
2724
- resource "aws_scheduler_schedule" "wiki_compile_drainer" {
2725
- count = local.deploy_lambda_handlers ? 1 : 0
2726
-
2727
- name = "thinkwork-${var.stage}-wiki-compile-drainer"
2728
- group_name = "default"
2729
- schedule_expression = "rate(1 minutes)"
2730
- state = "ENABLED"
2731
-
2732
- flexible_time_window {
2733
- mode = "OFF"
2734
- }
2735
-
2736
- target {
2737
- arn = aws_lambda_function.handler["wiki-compile"].arn
2738
- role_arn = aws_iam_role.scheduler.arn
2739
- }
2740
- }
2741
-
2742
- # Observations → Knowledge Graph sweep (plan 2026-06-09-004 U5). Enumerates
2743
- # tenants and runs an incremental observations ingest per tenant; the stable
2744
- # source_ref's active-run dedupe drops overlap with operator-started runs and
2745
- # the in-handler stale-run reaper clears stranded rows past the run ceiling.
2746
- resource "aws_scheduler_schedule" "knowledge_graph_observations_ingest" {
2747
- count = local.deploy_lambda_handlers ? 1 : 0
2748
-
2749
- name = "thinkwork-${var.stage}-knowledge-graph-observations-ingest"
2750
- group_name = "default"
2751
- schedule_expression = "rate(30 minutes)"
2752
- # Ships DISABLED (plan 2026-07-03-005 U4/KTD-6): the schedule enables on dev
2753
- # only after a manual golden-set-validated run. Driven by the GHA
2754
- # WIKI_KG_INGEST_ENABLED var.
2755
- state = var.knowledge_graph_observations_ingest_enabled ? "ENABLED" : "DISABLED"
2756
-
2757
- flexible_time_window {
2758
- mode = "OFF"
2759
- }
2760
-
2761
- target {
2762
- arn = aws_lambda_function.handler["knowledge-graph-observations-ingest"].arn
2763
- role_arn = aws_iam_role.scheduler.arn
2764
- input = jsonencode({ sweep = true, trigger = "scheduled" })
2765
-
2766
- # Periodic idempotent worker: the next 30-minute tick IS the retry.
2767
- # Scheduler-level retries stack extra invocations onto a cadence that
2768
- # already self-corrects (cursor-guarded), compounding the invocation
2769
- # volume that helped trip Lambda recursive-loop detection.
2770
- retry_policy {
2771
- maximum_retry_attempts = 0
2772
- }
2773
- }
2774
- }
2775
-
2776
- # Ontology suggestion scan sweep (THINK-320 U4/KTD-3). Enumerates tenants and
2777
- # starts a suggestion scan job per tenant; tenants with a pending/running scan
2778
- # are skipped in the handler, and the scan-job dedupe key drops same-bucket
2779
- # duplicate starts. Ships DISABLED — enabled per stage via
2780
- # ontology_scan_sweep_enabled after the Living Map review surfaces land.
2781
- resource "aws_scheduler_schedule" "ontology_scan_sweep" {
2494
+ # Identity drift match sweep (THINK-321 U7 / KTD-7 — R10). Enumerates
2495
+ # tenants with registered identity sources and starts a per-tenant match
2496
+ # job; tenants with a pending/running job are skipped in the handler and
2497
+ # the job dedupe key drops same-bucket duplicate starts. Ships DISABLED —
2498
+ # enabled per stage via identity_drift_match_enabled once bootstrap has
2499
+ # been proven on the stage.
2500
+ resource "aws_scheduler_schedule" "identity_drift_match" {
2782
2501
  count = local.deploy_lambda_handlers ? 1 : 0
2783
2502
 
2784
- name = "thinkwork-${var.stage}-ontology-scan-sweep"
2503
+ name = "thinkwork-${var.stage}-identity-drift-match"
2785
2504
  group_name = "default"
2786
2505
  schedule_expression = "rate(1 days)"
2787
- state = var.ontology_scan_sweep_enabled ? "ENABLED" : "DISABLED"
2506
+ state = var.identity_drift_match_enabled ? "ENABLED" : "DISABLED"
2788
2507
 
2789
2508
  flexible_time_window {
2790
2509
  mode = "OFF"
2791
2510
  }
2792
2511
 
2793
2512
  target {
2794
- arn = aws_lambda_function.handler["ontology-scan"].arn
2513
+ arn = aws_lambda_function.handler["identity-match"].arn
2795
2514
  role_arn = aws_iam_role.scheduler.arn
2796
- input = jsonencode({ sweep = true, trigger = "scheduled" })
2515
+ input = jsonencode({ drift = true, trigger = "scheduled" })
2797
2516
 
2798
- # Periodic idempotent worker: the next daily tick IS the retry (mirrors
2799
- # the knowledge_graph_observations_ingest precedent scheduler retries
2800
- # only stack invocations onto a self-correcting cadence).
2517
+ # Periodic idempotent worker: the next daily tick IS the retry
2518
+ # scheduler retries only stack invocations onto a self-correcting
2519
+ # cadence.
2801
2520
  retry_policy {
2802
2521
  maximum_retry_attempts = 0
2803
2522
  }
2804
2523
  }
2805
2524
  }
2806
2525
 
2807
- resource "aws_scheduler_schedule" "wiki_lint" {
2808
- count = local.deploy_lambda_handlers ? 1 : 0
2809
-
2810
- name = "thinkwork-${var.stage}-wiki-lint"
2811
- group_name = "default"
2812
- schedule_expression = "cron(0 2 * * ? *)" # daily at 02:00 UTC
2813
- state = "ENABLED"
2814
-
2815
- flexible_time_window {
2816
- mode = "OFF"
2817
- }
2818
-
2819
- target {
2820
- arn = aws_lambda_function.handler["wiki-lint"].arn
2821
- role_arn = aws_iam_role.scheduler.arn
2822
- }
2823
- }
2824
-
2825
- resource "aws_scheduler_schedule" "wiki_export" {
2826
- count = local.deploy_lambda_handlers ? 1 : 0
2827
-
2828
- name = "thinkwork-${var.stage}-wiki-export"
2829
- group_name = "default"
2830
- schedule_expression = "cron(0 3 * * ? *)" # daily at 03:00 UTC (after lint)
2831
- state = "ENABLED"
2832
-
2833
- flexible_time_window {
2834
- mode = "OFF"
2835
- }
2836
-
2837
- target {
2838
- arn = aws_lambda_function.handler["wiki-export"].arn
2839
- role_arn = aws_iam_role.scheduler.arn
2840
- }
2841
- }
2842
-
2843
- # S3 bucket for markdown vault exports. One bundle per (tenant, owner, date).
2844
- # Retention is handled by the lifecycle rule below (30 days).
2845
- resource "aws_s3_bucket" "wiki_exports" {
2846
- bucket = "thinkwork-${var.stage}-wiki-exports"
2847
- force_destroy = var.stage == "dev"
2848
-
2849
- tags = {
2850
- Name = "thinkwork-${var.stage}-wiki-exports"
2851
- }
2852
- }
2853
-
2854
- resource "aws_s3_bucket_public_access_block" "wiki_exports" {
2855
- bucket = aws_s3_bucket.wiki_exports.id
2856
- block_public_acls = true
2857
- block_public_policy = true
2858
- ignore_public_acls = true
2859
- restrict_public_buckets = true
2860
- }
2861
-
2862
- resource "aws_s3_bucket_lifecycle_configuration" "wiki_exports" {
2863
- bucket = aws_s3_bucket.wiki_exports.id
2864
-
2865
- rule {
2866
- id = "expire-old-bundles"
2867
- status = "Enabled"
2868
-
2869
- filter {}
2870
-
2871
- expiration {
2872
- days = 30
2873
- }
2874
- }
2875
- }
2876
-
2877
- # Canonical ThinkWork Brain artifact store. Unlike wiki_exports, this bucket
2526
+ # Canonical ThinkWork Brain artifact store. This bucket
2878
2527
  # is the durable replay/projection substrate for Brain source artifacts,
2879
2528
  # ingestion manifests, migration snapshots, vault projections, and exports.
2880
2529
  resource "aws_s3_bucket" "brain_artifacts" {
@@ -2997,7 +2646,7 @@ resource "aws_s3_bucket_lifecycle_configuration" "brain_artifacts" {
2997
2646
 
2998
2647
  # THINK-193 U1 (Codex F6): normalized evidence snapshots are short-lived
2999
2648
  # working copies — the durable record is the evidence row (hash + ref) and
3000
- # the Hindsight projection. Expire content after 30 days; the app mirrors
2649
+ # the memory projection. Expire content after 30 days; the app mirrors
3001
2650
  # this via memory_evidence_items.snapshot_expires_at.
3002
2651
  rule {
3003
2652
  id = "expire-evidence-snapshots"
@@ -3037,6 +2686,48 @@ resource "aws_s3_bucket_lifecycle_configuration" "brain_artifacts" {
3037
2686
  noncurrent_days = 365
3038
2687
  }
3039
2688
  }
2689
+
2690
+ # THINK-630: async brain_ask task records are a polling buffer, not an
2691
+ # archive — they hold answer content and principal metadata, so they must
2692
+ # not accumulate. Seven days far exceeds any task's useful life. The
2693
+ # brain-mcp module (company-brain repo) deliberately does NOT manage this
2694
+ # bucket's lifecycle (it would silently drop the rules above); this stack
2695
+ # is the bucket's single lifecycle author.
2696
+ rule {
2697
+ id = "expire-brain-ask-tasks"
2698
+ status = "Enabled"
2699
+
2700
+ filter {
2701
+ prefix = "brain-ask-tasks/"
2702
+ }
2703
+
2704
+ expiration {
2705
+ days = 7
2706
+ }
2707
+
2708
+ noncurrent_version_expiration {
2709
+ noncurrent_days = 7
2710
+ }
2711
+ }
2712
+
2713
+ # THINK-629 C2: ask exchange records (follow-up continuity) age out the
2714
+ # same way — the durable record is the thread itself.
2715
+ rule {
2716
+ id = "expire-brain-ask-exchanges"
2717
+ status = "Enabled"
2718
+
2719
+ filter {
2720
+ prefix = "brain-ask-exchanges/"
2721
+ }
2722
+
2723
+ expiration {
2724
+ days = 7
2725
+ }
2726
+
2727
+ noncurrent_version_expiration {
2728
+ noncurrent_days = 7
2729
+ }
2730
+ }
3040
2731
  }
3041
2732
 
3042
2733
  data "aws_iam_policy_document" "brain_artifacts_bucket" {
@@ -3121,33 +2812,6 @@ resource "aws_iam_role_policy" "scheduler_invoke" {
3121
2812
  # SSM Parameters — Lambda ARNs for cross-function invocation
3122
2813
  # ---------------------------------------------------------------------------
3123
2814
 
3124
- ########################################################################
3125
- # SecureString parameter for the Google Places API key. wiki-compile reads
3126
- # this on cold start via loadGooglePlacesClientFromSsm() and caches the
3127
- # client at module scope. When google_places_api_key is empty (the
3128
- # default), we seed the parameter with a placeholder so the Lambda init
3129
- # path can distinguish "unconfigured" (skip Google entirely, degrade
3130
- # gracefully) from "configured but wrong" (log + skip). lifecycle.ignore_
3131
- # changes on `value` lets ops rotate via
3132
- # aws ssm put-parameter --overwrite \
3133
- # --name /thinkwork/<stage>/google-places/api-key \
3134
- # --type SecureString --value <KEY>
3135
- # without terraform fighting it on the next apply.
3136
- ########################################################################
3137
-
3138
- resource "aws_ssm_parameter" "google_places_api_key" {
3139
- name = "/thinkwork/${var.stage}/google-places/api-key"
3140
- type = "SecureString"
3141
- value = var.google_places_api_key != "" ? var.google_places_api_key : "PLACEHOLDER_SET_VIA_CLI"
3142
- description = "Google Places API (New) key consumed by wiki-compile. See docs/plans/2026-04-21-005-feat-wiki-place-capability-v2-plan.md Unit 4."
3143
-
3144
- lifecycle {
3145
- # Allow `aws ssm put-parameter --overwrite` to stick across applies.
3146
- # New-key rotation or initial population by ops should happen via CLI,
3147
- # not via terraform var.
3148
- ignore_changes = [value]
3149
- }
3150
- }
3151
2815
 
3152
2816
  ########################################################################
3153
2817
  # SecureString parameter for the graphql-http Lambda's OWN Cloudflare
@@ -3191,8 +2855,10 @@ resource "aws_ssm_parameter" "cloudflare_namespace_token" {
3191
2855
 
3192
2856
  resource "aws_ssm_parameter" "lambda_arns" {
3193
2857
  for_each = local.deploy_lambda_handlers ? {
3194
- "chat-agent-invoke-fn-arn" = aws_lambda_function.handler["chat-agent-invoke"].arn
3195
- "kb-manager-fn-arn" = aws_lambda_function.handler["knowledge-base-manager"].arn
2858
+ # THINK-583 U3: alias-qualified so the SSM-fallback path in
2859
+ # getChatAgentInvokeFnArn (graphql/utils.ts) also lands on the
2860
+ # provisioned-concurrency alias, not $LATEST.
2861
+ "chat-agent-invoke-fn-arn" = aws_lambda_alias.live["chat-agent-invoke"].arn
3196
2862
  "job-schedule-manager-fn-arn" = aws_lambda_function.handler["job-schedule-manager"].arn
3197
2863
  "memory-retain-fn-arn" = aws_lambda_function.handler["memory-retain"].arn
3198
2864
  "eval-runner-fn-arn" = aws_lambda_function.handler["eval-runner"].arn
@@ -3251,6 +2917,17 @@ resource "aws_lambda_function" "compliance_anchor" {
3251
2917
  COMPLIANCE_ANCHOR_OBJECT_LOCK_MODE = var.compliance_anchor_object_lock_mode
3252
2918
  }
3253
2919
  }
2920
+
2921
+ # THINK-1082: this function opens a database connection (see env above), so
2922
+ # the whole-module VPC lane covers it too. Absent when lambdas_in_vpc is off.
2923
+ dynamic "vpc_config" {
2924
+ for_each = var.lambdas_in_vpc ? [1] : []
2925
+
2926
+ content {
2927
+ subnet_ids = var.vpc_subnet_ids
2928
+ security_group_ids = var.vpc_security_group_ids
2929
+ }
2930
+ }
3254
2931
  }
3255
2932
 
3256
2933
  resource "aws_sqs_queue" "compliance_anchor_dlq" {
@@ -3557,6 +3234,17 @@ resource "aws_lambda_function" "compliance_export_runner" {
3557
3234
  DATABASE_NAME = var.database_name
3558
3235
  }
3559
3236
  }
3237
+
3238
+ # THINK-1082: this function opens a database connection (see env above), so
3239
+ # the whole-module VPC lane covers it too. Absent when lambdas_in_vpc is off.
3240
+ dynamic "vpc_config" {
3241
+ for_each = var.lambdas_in_vpc ? [1] : []
3242
+
3243
+ content {
3244
+ subnet_ids = var.vpc_subnet_ids
3245
+ security_group_ids = var.vpc_security_group_ids
3246
+ }
3247
+ }
3560
3248
  }
3561
3249
 
3562
3250
  # SQS → Lambda event source mapping. batch_size=1 so each export is a