thinkwork-cli 0.13.1 → 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 (262) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
  3. package/dist/bowser-6Z6JFAIR.js +2822 -0
  4. package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
  5. package/dist/chunk-AD2NIWB5.js +6516 -0
  6. package/dist/chunk-DV3MRBR5.js +43 -0
  7. package/dist/chunk-EI5ZX7CN.js +522 -0
  8. package/dist/chunk-EUJ7X6YD.js +126 -0
  9. package/dist/chunk-GG5WKBOX.js +436 -0
  10. package/dist/chunk-I64EZSU5.js +358 -0
  11. package/dist/chunk-KMIJ5KGT.js +128 -0
  12. package/dist/chunk-MWQ7RO76.js +137 -0
  13. package/dist/chunk-OCPHU34C.js +996 -0
  14. package/dist/chunk-OLUQ4U2Z.js +1728 -0
  15. package/dist/chunk-XYM7PE2A.js +2264 -0
  16. package/dist/chunk-YQCYD6NE.js +3299 -0
  17. package/dist/cli.js +49156 -31257
  18. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  19. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  20. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  21. package/dist/dist-es-2OTNTL4N.js +315 -0
  22. package/dist/dist-es-2XVAH4BQ.js +14726 -0
  23. package/dist/dist-es-3DPDUJAH.js +2000 -0
  24. package/dist/dist-es-BNZTBBVC.js +176 -0
  25. package/dist/dist-es-CD2RBDAC.js +88 -0
  26. package/dist/dist-es-I4FW57PS.js +88 -0
  27. package/dist/dist-es-KL7Y5NB2.js +377 -0
  28. package/dist/dist-es-KVFWJNDY.js +318 -0
  29. package/dist/dist-es-PS73DWNL.js +70 -0
  30. package/dist/dist-es-S3NZQFGT.js +170 -0
  31. package/dist/dist-es-SGCA4P32.js +48 -0
  32. package/dist/dist-es-WBM6J4RA.js +70 -0
  33. package/dist/dist-es-ZGVADYIF.js +485 -0
  34. package/dist/dist-es-ZKC2QKD2.js +377 -0
  35. package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
  36. package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
  37. package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
  38. package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
  39. package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
  40. package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
  41. package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
  42. package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
  43. package/dist/drizzle/0230_analyst_rls.sql +28 -36
  44. package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
  45. package/dist/drizzle/0251_brain_stragglers.sql +21 -0
  46. package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
  47. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  48. package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
  49. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  50. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  51. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  52. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  53. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  54. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  55. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  56. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  57. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  58. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  59. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  60. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  61. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  62. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  63. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  64. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  65. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  66. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  67. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  68. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  69. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  70. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  71. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  72. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  73. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  74. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  75. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  76. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  77. package/dist/drizzle/0287_brain_subject.sql +25 -0
  78. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  79. package/dist/event-streams-7JNLJEEL.js +894 -0
  80. package/dist/event-streams-KDF5RWR6.js +1515 -0
  81. package/dist/loadSso-ETCXTR4I.js +576 -0
  82. package/dist/loadSso-UBD7KUHE.js +583 -0
  83. package/dist/plugins/README.md +2 -2
  84. package/dist/plugins/catalog/package.json +1 -5
  85. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
  86. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  87. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
  88. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
  89. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
  90. package/dist/plugins/catalog/src/contracts.ts +17 -179
  91. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
  92. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  93. package/dist/plugins/twenty/package.json +2 -1
  94. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  95. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  96. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  97. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  98. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  99. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  100. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  101. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  102. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  103. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  104. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  105. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  106. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  107. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  108. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  109. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  110. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  111. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  112. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  113. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  114. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  115. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  116. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  117. package/dist/scripts/post-deploy.sh +79 -1
  118. package/dist/signin-5QZNRSOO.js +821 -0
  119. package/dist/sso-oidc-XK673P54.js +837 -0
  120. package/dist/sts-OXI2UALF.js +1113 -0
  121. package/dist/sts-SQW4NBK3.js +1117 -0
  122. package/dist/terraform/examples/greenfield/main.tf +391 -176
  123. package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
  124. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  125. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  126. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  127. package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
  128. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  129. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
  130. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
  131. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  132. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  133. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  134. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
  135. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  136. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
  137. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
  138. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  139. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  140. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  141. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  142. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  143. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  144. package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
  145. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  146. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  147. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  148. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  149. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  150. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  151. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  152. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  153. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  154. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  155. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  156. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  157. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  158. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  159. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  160. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  161. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  162. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  163. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  164. package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
  165. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
  166. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  167. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  168. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  169. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  170. package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
  171. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
  172. package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
  173. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  174. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  175. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
  176. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  177. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  178. package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
  179. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  180. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  181. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  182. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  183. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  184. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  185. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  186. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  187. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  188. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  189. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  190. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  191. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  192. package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
  193. package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
  194. package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
  195. package/dist/terraform/modules/thinkwork/README.md +5 -1
  196. package/dist/terraform/modules/thinkwork/main.tf +459 -234
  197. package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
  198. package/dist/terraform/modules/thinkwork/variables.tf +353 -89
  199. package/dist/terraform/schema.graphql +69 -36
  200. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  201. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  202. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  203. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  204. package/dist/workspace-defaults/files/USER.md +1 -0
  205. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  206. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  207. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  208. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  209. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  210. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  211. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  212. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  213. package/package.json +3 -2
  214. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
  215. package/dist/plugins/company-data/README.md +0 -49
  216. package/dist/plugins/company-data/package.json +0 -21
  217. package/dist/plugins/company-data/src/index.ts +0 -30
  218. package/dist/plugins/company-data/src/manifest.ts +0 -26
  219. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  220. package/dist/plugins/company-data/tsconfig.json +0 -8
  221. package/dist/plugins/company-etl/README.md +0 -46
  222. package/dist/plugins/company-etl/package.json +0 -21
  223. package/dist/plugins/company-etl/src/index.ts +0 -29
  224. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  225. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  226. package/dist/plugins/company-etl/tsconfig.json +0 -8
  227. package/dist/plugins/lastmile/README.md +0 -50
  228. package/dist/plugins/lastmile/package.json +0 -23
  229. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  230. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  231. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  232. package/dist/plugins/lastmile/src/index.ts +0 -46
  233. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  234. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  235. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  236. package/dist/plugins/lastmile/tsconfig.json +0 -8
  237. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  238. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  239. package/dist/plugins/workos-auth/package.json +0 -22
  240. package/dist/plugins/workos-auth/src/index.ts +0 -30
  241. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  242. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  243. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  244. package/dist/plugins/workos-auth/tsconfig.json +0 -8
  245. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  246. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  247. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  248. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  249. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
  250. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
  251. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
  252. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  253. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  254. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  255. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  256. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  257. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  258. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  259. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  260. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  261. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  262. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -14,6 +14,12 @@ locals {
14
14
  # deployment jobs — plan 2026-06-12-001 U10); the TWENTY config key is
15
15
  # retired.
16
16
  optional_integration_handler_names = concat(
17
+ var.auth_retirement_phase == "retired" ? [
18
+ # The native login UI never publishes this handler. It remains reachable
19
+ # only as an operator-controlled rollback seam through coexistence and
20
+ # cutover/soak, then Terraform removes the Lambda and routes atomically.
21
+ "workos-auth",
22
+ ] : [],
17
23
  var.deployment_control_plane_enabled ? [] : [
18
24
  # Host-only onboarding/deployment API. Customer foundations disable the
19
25
  # deployment control plane, so release-based customer installs must not
@@ -47,49 +53,59 @@ locals {
47
53
  # The reader-coverage fixture test in apps/cli fails CI if a key in this
48
54
  # map still has a direct process.env reader. Identity values (STAGE,
49
55
  # AWS_ACCOUNT_ID, NODE_OPTIONS) and secrets (DATABASE_URL,
50
- # API_AUTH_SECRET, APPSYNC_API_KEY) stay out of this map — identity stays
56
+ # API_AUTH_SECRET) stay out of this map — identity stays
51
57
  # env forever, secrets live in Secrets Manager (R4), never in the String
52
58
  # document.
53
59
  config_env = merge({
54
60
  # THINK-173: PUBLIC verification key (not a secret) + the NAME of the
55
61
  # private-key secret (resolved via runtime-config's secret loader —
56
62
  # the PEM itself never enters the env or the String document).
57
- CAPABILITY_SIGNING_PUBLIC_KEY = var.capability_signing_public_key
58
- CAPABILITY_SIGNING_PRIVATE_KEY_SECRET = var.capability_signing_private_key_secret
59
- # THINK-229 U3/KTD5 — the analyst policy-source enforcement flip.
60
- # "row" (default) keeps sidecar policy shadow-only; "sidecar" makes the
61
- # signed sidecar block authoritative (budgets/policyClaims flow). Flip
62
- # ONLY after clean shadow parity on live traffic.
63
- ANALYST_POLICY_SOURCE = var.analyst_policy_source
64
- # Governed autonomy per-tenant opt-in allowlist for autonomous capability
65
- # self-extension (the two self_admit_connection / self_approve_routine
66
- # actions on capability-control-service). Comma/space-separated tenant ids;
67
- # empty (default) = NO tenant enabled, so self-extension ships inert and
68
- # fail-closed. Read via getConfig() (env-wins) in isSelfExtensionEnabled.
69
- CAPABILITY_SELF_EXTENSION_TENANTS = var.capability_self_extension_tenants
70
- # THINK-230 the operator-facing provisionAnalystConnector mutation runs
71
- # the analyst connector provisioning ceremony inside graphql-http, so the
72
- # shared api handlers read the same broker-secret ARN + rds_iam connect
73
- # config the analyst-query-broker handler carries per-handler. Read via
74
- # getConfig() (env-wins), never process.env (runtime-config fixture gate).
75
- # ANALYST_DB_CLUSTER_ENDPOINT gates on the resource id so a stage without
76
- # the IAM grant leaves rds_iam provisioning off (resolveAnalystRdsIamConfig
77
- # returns null) instead of half-seeding a credential row.
78
- ANALYST_BROKER_SECRET_ARN = var.analyst_broker_secret_arn
79
- ANALYST_DB_CLUSTER_ENDPOINT = var.analyst_db_cluster_resource_id != "" ? var.db_cluster_endpoint : ""
80
- ANALYST_DB_CLUSTER_RESOURCE_ID = var.analyst_db_cluster_resource_id
81
- ANALYST_DB_NAME = var.database_name
82
- ANALYST_DB_USER = "analyst_reader"
83
- DATABASE_SECRET_ARN = var.graphql_db_secret_arn
84
- DATABASE_HOST = var.db_cluster_endpoint
85
- DATABASE_NAME = var.database_name
63
+ CAPABILITY_SIGNING_PUBLIC_KEY = var.capability_signing_public_key
64
+ CAPABILITY_SIGNING_PRIVATE_KEY_SECRET = var.capability_signing_private_key_secret
65
+ SUBSCRIPTION_TICKET_SIGNING_KEY_ID = var.subscription_ticket_signing_key_id
66
+ SUBSCRIPTION_TICKET_PUBLIC_KEYS = var.subscription_ticket_public_keys
67
+ SUBSCRIPTION_TICKET_PRIVATE_KEY_SECRET = var.subscription_ticket_private_key_secret
68
+ APPSYNC_API_ID = var.appsync_api_id
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
86
102
  # THINK-280 U8: the LAST rollout gate. External capability search + the
87
103
  # Inspector governed-runtime operation layer stay inert until the broker is
88
104
  # enabled for the stage. Off by default — read via getConfig() (env-wins).
89
105
  CAPABILITY_EXTERNAL_SEARCH_ENABLED = var.enable_capability_broker ? "true" : "false"
90
106
  ENABLE_CAPABILITY_BROKER = var.enable_capability_broker ? "true" : "false"
91
107
  # BUCKET_NAME and USER_POOL_ID were duplicate aliases of WORKSPACE_BUCKET
92
- # and COGNITO_USER_POOL_ID; GRAPHQL_API_KEY duplicated APPSYNC_API_KEY;
108
+ # and COGNITO_USER_POOL_ID; the legacy GRAPHQL_API_KEY alias is retired;
93
109
  # THINKWORK_API_SECRET and EMAIL_HMAC_SECRET duplicated API_AUTH_SECRET
94
110
  # (~310 serialized bytes total). graphql-http sits at Lambda's hard 4KB
95
111
  # env ceiling (#2375) — every reader falls back to the canonical name.
@@ -101,9 +117,17 @@ locals {
101
117
  COGNITO_AUTH_BASE_URL = local.mcp_oauth_cognito_base_url
102
118
  MCP_OAUTH_CALLBACK_URL = "${local.mcp_oauth_api_base_url}/mcp/oauth/callback"
103
119
  MCP_OAUTH_REVOCATIONS_TABLE = aws_dynamodb_table.mcp_oauth_revocations.name
104
- COGNITO_APP_CLIENT_IDS = "${var.admin_client_id},${var.mobile_client_id}"
105
- APPSYNC_ENDPOINT = var.appsync_api_url
106
- 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
107
131
  # Deterministic routines v1 (plan 2026-07-03-004 U6): activates the
108
132
  # git-backed routine lifecycle tool suite on the admin-ops MCP server
109
133
  # (routine_repo_list/read/commit, routine_run_fixtures, routine_runs).
@@ -137,13 +161,11 @@ locals {
137
161
  # WORKSPACE_RENDERER_FUNCTION_NAME is derived from the per-stage naming
138
162
  # convention by deriveFunctionName("workspace-renderer") — stored
139
163
  # nowhere (R7).
140
- WORKSPACE_BUCKET = var.bucket_name
141
- HINDSIGHT_ENDPOINT = var.hindsight_endpoint
142
- # THINK-220 cutover flag: empty = hindsight schema on the primary DB;
143
- # set = that database's public schema via the database-pg seam.
144
- HINDSIGHT_DATABASE_NAME = var.hindsight_database_name
145
- AGENTCORE_MEMORY_ID = var.agentcore_memory_id
146
- 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"
147
169
  # CHAT_AGENT_INVOKE_FN_ARN (~112 serialized bytes) was dropped for the
148
170
  # 4KB env ceiling (#2375): getChatAgentInvokeFnArn and managed-dispatch
149
171
  # now derive the ARN from the deterministic naming pattern
@@ -159,7 +181,6 @@ locals {
159
181
  # deploymentStatus reads ADMIN_URL directly — so the single canonical
160
182
  # ADMIN_URL is sufficient. Re-add aliases only after env vars move to SSM.
161
183
  ADMIN_URL = var.admin_url
162
- DOCS_URL = var.docs_url
163
184
  APPSYNC_REALTIME_URL = var.appsync_realtime_url
164
185
  ECR_REPOSITORY_URL = var.ecr_repository_url
165
186
  # Per-user OAuth wiring (Google Workspace today; Microsoft 365 follow-up).
@@ -231,18 +252,22 @@ locals {
231
252
  # window (R8). Config-class keys live ONLY in the SSM runtime-config
232
253
  # document now — adding a key here is guarded by the identity-allowlist
233
254
  # fixture test in apps/cli (R10). Follow-up release: DATABASE_URL,
234
- # APPSYNC_API_KEY, and API_AUTH_SECRET drop too (readers already resolve
255
+ # API_AUTH_SECRET drops too (readers already resolve
235
256
  # via Secrets Manager prefetch when the env copies are absent), bringing
236
257
  # every handler under the ≤1KB R1 target.
237
258
  common_env = {
238
259
  STAGE = var.stage
239
260
  DATABASE_URL = "postgresql://${var.db_username}:${urlencode(var.db_password)}@${var.db_cluster_endpoint}:5432/${var.database_name}?sslmode=no-verify"
240
- APPSYNC_API_KEY = var.appsync_api_key
241
261
  API_AUTH_SECRET = var.api_auth_secret
242
262
  AWS_ACCOUNT_ID = var.account_id
243
263
  NODE_OPTIONS = "--enable-source-maps"
244
264
  }
245
265
 
266
+ # THINK-316 U1: the assertion mint runs on a sibling role and receives only
267
+ # the database connection plus immutable stage identity. In particular it
268
+ # does not inherit API_AUTH_SECRET or APPSYNC_API_KEY from common_env.
269
+
270
+
246
271
  # Per-handler env-var overrides. ARNs are constructed from the naming
247
272
  # pattern (same trick as the api-cross-function-invoke statement in
248
273
  # iam-grouped.tf) so we don't introduce a self-referential dependency
@@ -256,39 +281,26 @@ locals {
256
281
  }
257
282
 
258
283
  handler_extra_env = {
259
- # Analyst query broker (THINK-228 U3). Reader role + caller credential
260
- # secrets, and the workspace bucket's analyst-staging/ prefix for
261
- # large-result CSVs (lifecycle TTL lives on the bucket module). The
262
- # shared writer DATABASE_SECRET_ARN also lands in this handler's env
263
- # via common_env, but the broker code never uses it — SQL runs only as
264
- # analyst_reader.
265
- "analyst-query-broker" = {
266
- ANALYST_READER_SECRET_ARN = var.analyst_reader_secret_arn
267
- ANALYST_BROKER_SECRET_ARN = var.analyst_broker_secret_arn
268
- ANALYST_STAGING_BUCKET = var.bucket_name
269
- ANALYST_STAGING_PREFIX = "analyst-staging"
270
- # THINK-229 U1: RDS IAM connect config. Endpoint presence switches
271
- # analyst-reader-db.ts to the IAM-token path (password secret above
272
- # is the pre-GRANT-rds_iam fallback, retired once IAM is proven).
273
- # Gated on the resource ID so a stage without the IAM grant keeps
274
- # the password path instead of failing into the fallback every cold
275
- # start.
276
- ANALYST_DB_CLUSTER_ENDPOINT = var.analyst_db_cluster_resource_id != "" ? var.db_cluster_endpoint : ""
277
- ANALYST_DB_CLUSTER_RESOURCE_ID = var.analyst_db_cluster_resource_id
278
- ANALYST_DB_NAME = var.database_name
279
- ANALYST_DB_USER = "analyst_reader"
280
- }
281
- # THINK-229 U5 — the connection reconciler probes the analyst_reader
282
- # connection EXACTLY as the broker does (getAnalystReaderClient), so it
283
- # needs the same IAM-connect config + the password-fallback secret. The
284
- # shared lambda execution role already holds rds-db:connect on the
285
- # analyst_reader dbuser (iam-grouped.tf, granted in U1), so no extra IAM.
286
- "analyst-connection-reconciler" = {
287
- ANALYST_READER_SECRET_ARN = var.analyst_reader_secret_arn
288
- ANALYST_DB_CLUSTER_ENDPOINT = var.analyst_db_cluster_resource_id != "" ? var.db_cluster_endpoint : ""
289
- ANALYST_DB_CLUSTER_RESOURCE_ID = var.analyst_db_cluster_resource_id
290
- ANALYST_DB_NAME = var.database_name
291
- ANALYST_DB_USER = "analyst_reader"
284
+ "skills" = {
285
+ AGENTCORE_IDENTITY_WORKLOAD_NAME = "thinkwork-${var.stage}-multiplayer-proof"
286
+ AGENTCORE_TWENTY_CREDENTIAL_PROVIDER_NAME = "thinkwork-${var.stage}-twenty-crm"
287
+ AGENTCORE_USER_OAUTH_RETURN_URL = "${local.mcp_oauth_api_base_url}/api/skills/mcp-oauth/agentcore/complete"
288
+ AGENTCORE_TWENTY_OAUTH_RESOURCE = "https://crm.thinkwork.ai/mcp"
289
+ }
290
+ "workos-auth" = {
291
+ # During cutover/soak the callback, bridge, and logout paths remain for
292
+ # already-issued state, but the handler independently refuses every new
293
+ # WorkOS authorize start. Terraform removes the handler in retired.
294
+ AUTH_RETIREMENT_PHASE = var.auth_retirement_phase
295
+ }
296
+ "public-auth-options" = {
297
+ AUTH_RETIREMENT_PHASE = var.auth_retirement_phase
298
+ }
299
+ "auth-me" = {
300
+ AUTH_MIGRATION_RECOVERY_DEADLINE = var.auth_migration_recovery_deadline
301
+ }
302
+ "auth-enrollment" = {
303
+ AUTH_MIGRATION_RECOVERY_DEADLINE = var.auth_migration_recovery_deadline
292
304
  }
293
305
  # THINK-246: customer stages cannot send as the dev fallback domain
294
306
  # (noreply@agents.thinkwork.ai is only verified in the dev account) —
@@ -332,46 +344,6 @@ locals {
332
344
  CAPABILITY_BROKER_VPCE_DNS = var.capability_broker_vpce_dns
333
345
  CAPABILITY_BROKER_SESSION_TABLE = var.capability_broker_session_table
334
346
  }
335
- # Compounding Memory compile Lambda. Any Converse-compatible Bedrock
336
- # model works; the planner + section-writer cap themselves at ~500
337
- # records / 25 new pages per invocation so a 480 s timeout covers
338
- # the worst case comfortably. Env vars come from variables so
339
- # unrelated deploys don't wipe them back to defaults (the aggregation
340
- # flag got reset on every terraform apply before this was pinned).
341
- "wiki-compile" = {
342
- BEDROCK_MODEL_ID = var.wiki_compile_model_id
343
- WIKI_AGGREGATION_PASS_ENABLED = var.wiki_aggregation_pass_enabled
344
- WIKI_DETERMINISTIC_LINKING_ENABLED = var.wiki_deterministic_linking_enabled
345
- # Wiki pipeline source dispatch (plan 2026-06-09-004 U10):
346
- # 'planner' (default, LLM compile) | 'graph' (deterministic
347
- # graph→wiki materializer over the knowledge-graph mirror).
348
- WIKI_SOURCE = var.wiki_source
349
- # Name (not value) of the SecureString SSM parameter that holds the
350
- # Google Places API key. wiki-compile fetches + caches on cold start.
351
- # The parameter may contain a placeholder value at apply time — the
352
- # Lambda logs and degrades gracefully if decryption returns empty.
353
- GOOGLE_PLACES_SSM_PARAM_NAME = "/thinkwork/${var.stage}/google-places/api-key"
354
- # THINK-200 chain: a successful compile Event-invokes okf-materialize
355
- # so the OKF projection (and, chained, the Pi navigator's EFS view)
356
- # stays current without manual invocation.
357
- OKF_MATERIALIZE_FN_NAME = "thinkwork-${var.stage}-api-okf-materialize"
358
- }
359
- "ontology-scan" = {
360
- BEDROCK_MODEL_ID = var.wiki_compile_model_id
361
- }
362
- "wiki-export" = {
363
- WIKI_EXPORT_BUCKET = aws_s3_bucket.wiki_exports.bucket
364
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
365
- }
366
- "okf-materialize" = {
367
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
368
- # THINK-200 chain: fresh bundles fan out to the EFS current view.
369
- OKF_EFS_REFRESH_FN_NAME = "thinkwork-${var.stage}-api-okf-efs-refresh"
370
- }
371
- "okf-efs-refresh" = {
372
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
373
- OKF_EFS_ROOT = var.okf_efs_mount_path
374
- }
375
347
  # THINK-193 U1 (Codex F6): normalized evidence snapshots live in the
376
348
  # encrypted brain-artifacts bucket under evidence-snapshots/ — Postgres
377
349
  # keeps only the s3:// ref + hash. 30-day lifecycle expiration below.
@@ -402,56 +374,72 @@ locals {
402
374
  CONTEXT_ENGINE_MEMORY_QUERY_MODE = "reflect"
403
375
  CONTEXT_ENGINE_MEMORY_TIMEOUT_MS = "20000"
404
376
  }
405
- # Agent graph access (plan 2026-06-09-004 U8): stage gate for the Pi
406
- # 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
+ }
407
389
  "chat-agent-invoke" = {
408
- KNOWLEDGE_GRAPH_TOOL_ENABLED = tostring(var.knowledge_graph_tool_enabled)
409
- }
410
- # 240s: sync Hindsight retain (LLM extraction + auto-consolidation) can
411
- # exceed 60s; the client timeout must stay below the Lambda timeout (300s)
412
- # and below the Hindsight ALB idle_timeout (300s) so failures classify as
413
- # client timeouts, never ALB 504s.
414
- "memory-retain" = {
415
- HINDSIGHT_TIMEOUT_MS = "240000"
416
- }
417
- "brain-dream-state" = {
418
- BRAIN_DREAM_STATE_ENABLED = tostring(var.brain_dream_state_enabled)
419
- }
420
- # THINK-307 KTD2: the origin-freshness guard reads the same stall
421
- # threshold as the stall monitor one operational knob, two consumers,
422
- # no drift between "stalled" and "recovered".
423
- "cron-retry-dispatcher" = {
424
- STALL_THRESHOLD_MINUTES = tostring(var.stall_threshold_minutes)
425
- }
426
- # Bedrock KB provisioning. Per-handler (not common_env) so these don't bloat
427
- # the already-near-4KB graphql-http env. Bedrock's RDS-backed KB needs the
428
- # cluster ARN + the KB service role (passed at CreateKnowledgeBase time).
429
- "knowledge-base-manager" = {
430
- KB_SERVICE_ROLE_ARN = var.kb_service_role_arn
431
- DATABASE_CLUSTER_ARN = var.db_cluster_arn
432
- }
433
- # Observations Knowledge Graph worker. Extraction is now a Bedrock
434
- # structured-output call inside this Lambda. KG_EXTRACTION_MODEL_ID pins
435
- # the gpt-oss extraction model (Bedrock IAM via the shared lambda_bedrock
436
- # invoke policy, same as the promotion-gate classifier).
437
- "knowledge-graph-observations-ingest" = {
438
- BRAIN_ARTIFACTS_BUCKET = aws_s3_bucket.brain_artifacts.bucket
439
- OBSERVATION_CLASSIFIER_MODEL_ID = var.observation_classifier_model_id
440
- KG_EXTRACTION_MODEL_ID = var.kg_extraction_model_id
441
- # Per-run candidate cap bounds classifier + extraction cost; a
442
- # truncated backlog drains via an in-process loop inside one
443
- # invocation (never Lambda self-invoke AWS recursive-loop
444
- # detection terminates worker-to-self Event chains).
445
- KG_OBS_MAX_CANDIDATES_PER_RUN = var.kg_obs_max_candidates_per_run
446
- # THINK-193 U4 dead-handoff fix: a successful observations ingest
447
- # enqueues the tenant graph wiki-compile job inside its own commit
448
- # and Event-invokes wiki-compile post-commit. WIKI_SOURCE must match
449
- # the wiki-compile handler's dispatch flag (stage-global); the
450
- # per-tenant kill switch stays tenants.wiki_compile_enabled. The
451
- # invoke rides the shared lambda role's existing wiki-compile
452
- # lambda:InvokeFunction grant (iam-grouped.tf); the function name is
453
- # derived from STAGE (common_env) at call time.
454
- WIKI_SOURCE = var.wiki_source
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)
397
+ # THINK-311 U5: no HARNESS_RUNNER_FUNCTION_NAME env — derivable
398
+ # thinkwork-<stage>-api-* names never ride env (R1/R10 identity-only
399
+ # rule); resolveRuntimeFunctionName derives it from STAGE at call
400
+ # time, exactly like workspace-renderer.
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
+ }
428
+ # THINK-316 U5: the runner reads the server-only attested profile and
429
+ # invokes its named endpoint with a purpose-bound CUSTOM_JWT. It receives
430
+ # no Harness control-plane inputs or permissions.
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
455
443
  }
456
444
  # routine-task-python (Phase B U6) needs the AgentCore code-interpreter
457
445
  # id + the per-stage S3 routine-output bucket. The interpreter id is
@@ -587,10 +575,17 @@ resource "aws_lambda_function" "skill_trust_runner" {
587
575
  # Helper: creates Lambda functions from a local zip directory or release S3 keys
588
576
  # ---------------------------------------------------------------------------
589
577
 
590
- resource "aws_lambda_function" "handler" {
591
- 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([
592
583
  "graphql-http",
593
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",
594
589
  # Mobile agent harness: cloud Bedrock Converse proxy + completed-turn
595
590
  # persistence. Routes for these live in local.api_routes; the function
596
591
  # names must also be listed here (this set is the for_each source for
@@ -611,13 +606,6 @@ resource "aws_lambda_function" "handler" {
611
606
  # Client Engagement app API. Calls Twenty REST API directly server-side;
612
607
  # it is not backed by ThinkWork GraphQL or the agent MCP runtime.
613
608
  "twenty-client-engagement",
614
- # Desktop-local Pi tombstone endpoints. Kept temporarily so old packaged
615
- # desktop clients receive a stable 410 while all supported Pi execution
616
- # routes through managed AgentCore.
617
- "desktop-runtime-session",
618
- "desktop-workspace-prewarm",
619
- "managed-delegation",
620
- "desktop-eval-runs",
621
609
  # chat-agent-finalize — POST /api/threads/{threadId}/finalize. The
622
610
  # AgentCore runtime POSTs here at end-of-turn so the post-AgentCore
623
611
  # bookkeeping (cost recording, message insert, AppSync notify,
@@ -626,6 +614,14 @@ resource "aws_lambda_function" "handler" {
626
614
  # Bearer API_AUTH_SECRET. Idempotent on thread_turns.finalized_at
627
615
  # (migration 0123). Plan: 2026-05-22-006.
628
616
  "chat-agent-finalize",
617
+ # agents. The chat dispatch selector (resolveRuntimeFunctionName) routes
618
+ # a trial agent's turn here instead of the Pi container Lambda; the
619
+ # runner projects the payload into an AWS AgentCore Harness config,
620
+ # drives InvokeHarness, fulfills emit_document, and finalizes the turn
621
+ # via processFinalize. No API route — direct Event invoke only.
622
+ # THINK-316 U1: direct-invoke-only assertion mint. It derives all identity
623
+ # claims from the persisted turn tuple and signs with a dedicated KMS key;
624
+ # there is intentionally no public API Gateway route.
629
625
  # canvas-refresh — headless Living Artifacts data-refresh (THINK-145 U6).
630
626
  # Invoked RequestResponse by the refreshCanvasData mutation (graphql-http)
631
627
  # and by job-trigger's canvas_refresh branch (U7). Re-runs the saved
@@ -659,7 +655,13 @@ resource "aws_lambda_function" "handler" {
659
655
  "stripe-subscription",
660
656
  "deployment-sessions",
661
657
  "auth-me",
658
+ "auth-revoke",
659
+ "auth-enrollment",
660
+ "auth-subscription-ticket",
661
+ "appsync-subscription-authorizer",
662
+ "subscription-invalidation",
662
663
  "public-auth-options",
664
+ "auth-provider-reconcile",
663
665
  "workos-auth",
664
666
  # Public artifact share links (THINK-208): GET /share/{token}, token
665
667
  # verified in handler code (no gateway auth, uniform 404 on any miss).
@@ -675,6 +677,9 @@ resource "aws_lambda_function" "handler" {
675
677
  "mcp-open-engine",
676
678
  # THINK-280 U8: scoped external capability search MCP facade.
677
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",
678
683
  "activity",
679
684
  "routines",
680
685
  "budgets",
@@ -689,16 +694,7 @@ resource "aws_lambda_function" "handler" {
689
694
  "webhooks-admin",
690
695
  "webhook-deliveries-cleanup",
691
696
  "skill-runs-reconciler",
692
- # THINK-229 U5 — probes the analyst_reader connection (reachability, IAM
693
- # auth, SELECT-grant introspection, schema drift; all read-only) every 30
694
- # min and stamps the verdict onto runtime_metadata.analyst_probe so
695
- # dispatch can withhold a failing/stale connection loudly.
696
- "analyst-connection-reconciler",
697
697
  "cron-stall-monitor",
698
- # THINK-307: drains retry_queue rows the stall monitor enqueues. Built by
699
- # scripts/build-lambdas.sh since PRD-09 but first registered here; its
700
- # schedule ships DISABLED (var.retry_dispatcher_enabled default false).
701
- "cron-retry-dispatcher",
702
698
  "webhook-crm-opportunity",
703
699
  "webhook-task-event",
704
700
  "workspace-files",
@@ -706,8 +702,6 @@ resource "aws_lambda_function" "handler" {
706
702
  # 2026-06-12-002 U4). Bearer API_AUTH_SECRET + x-tenant-id; the runtime
707
703
  # fetch tool authorizes here, then downloads the returned S3 keys itself.
708
704
  "workspace-fetch-source",
709
- "knowledge-base-manager",
710
- "knowledge-base-files",
711
705
  "email-send",
712
706
  "email-inbound",
713
707
  "email-provider-webhook",
@@ -718,9 +712,7 @@ resource "aws_lambda_function" "handler" {
718
712
  "msteams-install-complete",
719
713
  "msteams-account-link-complete",
720
714
  "github-app",
721
- "memory",
722
715
  "memory-retain",
723
- "brain-dream-state",
724
716
  "memory-stage-worker",
725
717
  # THINK-193 U2 (Codex P1 #3): scheduled retry drainer for the retraction
726
718
  # saga ledger. Claims due/stale memory_retraction_attempts rows with the
@@ -735,15 +727,15 @@ resource "aws_lambda_function" "handler" {
735
727
  # terminal step event + SendTaskFailure. rate(15 minutes) schedule
736
728
  # (dedicated resource below).
737
729
  "memory-stage-sweeper",
738
- "wiki-compile",
739
- "knowledge-graph-observations-ingest",
740
- "ontology-scan",
741
- "ontology-reprocess",
742
- "wiki-lint",
743
- "wiki-export",
744
- "okf-materialize",
745
- "okf-efs-refresh",
746
- "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",
747
739
  "artifact-deliver",
748
740
  "recipe-refresh",
749
741
  "agent-skills-list",
@@ -830,16 +822,14 @@ resource "aws_lambda_function" "handler" {
830
822
  # Admin-Ops MCP — JSON-RPC endpoint at POST /mcp/admin, exposes the
831
823
  # @thinkwork/admin-ops package as MCP tools for managed agents.
832
824
  "admin-ops-mcp",
833
- # Analyst query broker — first-party MCP server at POST /mcp/analyst
834
- # (THINK-228 U3). One tool, query: EXPLAIN-gated, extended-protocol
835
- # single-statement SQL as the analyst_reader role; large results stage
836
- # to S3 under analyst-staging/; every query emits a data.query_executed
837
- # compliance audit event via POST /api/compliance/events.
838
- "analyst-query-broker",
839
825
  # MCP admin key management — per-tenant Bearer tokens for admin-ops.
840
826
  # Admin-ops-mcp authenticates incoming tokens by sha256-hash lookup
841
827
  # against tenant_mcp_admin_keys, populated by this handler's routes.
842
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",
843
833
  # One-shot tenant provisioning: mints a tkm_ key + stores in Secrets
844
834
  # Manager at thinkwork/<stage>/mcp/<tenantId>/admin-ops + upserts
845
835
  # tenant_mcp_servers. SM IAM is already granted on thinkwork/* by
@@ -854,6 +844,16 @@ resource "aws_lambda_function" "handler" {
854
844
  # Daily sweeper: auto-rejects MCP servers pending > 30 days. Triggered
855
845
  # by EventBridge schedule (mcp-approval-sweeper-daily).
856
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",
857
857
  # Finance pilot U2 — thread-attachment upload (presign + finalize).
858
858
  # presign issues a 5-min PUT URL the end-user client uses to push
859
859
  # Excel/CSV bytes directly to S3; finalize sniffs magic bytes, scans
@@ -863,6 +863,11 @@ resource "aws_lambda_function" "handler" {
863
863
  # threads.tenant_id lookup. Needs WORKSPACE_BUCKET env for S3.
864
864
  "thread-attachments-presign",
865
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",
866
871
  # U9-remainder of finance pilot — tenant-pinned download endpoint.
867
872
  # GET /api/threads/{tid}/attachments/{aid}/download returns a 302
868
873
  # to a 5-minute presigned S3 GET URL with ResponseContentDisposition:
@@ -877,14 +882,10 @@ resource "aws_lambda_function" "handler" {
877
882
  # POSTs one row per agent-session-start. Shared
878
883
  # API_AUTH_SECRET bearer (runtime→API; no tenant OAuth).
879
884
  "manifest-log",
880
- # Governed capability runtime control-plane service (THINK-280 U2).
881
- # NO HTTP route: the Pi container invokes it directly (RequestResponse)
882
- # via lambda:InvokeFunction over the approved in-account path — public
883
- # execute-api is not reliable from Pi's private VPC. Identity comes
884
- # exclusively from the Ed25519-signed capability caller context (the
885
- # CAPABILITY_SIGNING_PUBLIC_KEY in the shared runtime-config document
886
- # verifies it); plaintext payload fields are never trusted.
887
- "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",
888
889
  # SI-7 catalog-list read endpoint (plan §U15 pt 3/3). The runtime
889
890
  # fetches the allowed builtin-tool slug set once per
890
891
  # session-start + feature-flag-gated enforcement filter drops
@@ -924,7 +925,39 @@ resource "aws_lambda_function" "handler" {
924
925
  # new standalone resource (see U8b plan operator-step section).
925
926
  ]), toset(local.optional_integration_handler_names)) : toset([])
926
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
+
927
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)
928
961
  # S2 (THINK-193 U2): memory-retraction-drainer runs under a DEDICATED role
929
962
  # so its destructive evidence-snapshot S3 capability (version enumeration +
930
963
  # bulk version deletion) never leaks into the 90+ handlers on the shared
@@ -942,14 +975,10 @@ resource "aws_lambda_function" "handler" {
942
975
  depends_on = [
943
976
  aws_ssm_parameter.runtime_config,
944
977
  aws_secretsmanager_secret_version.api_auth,
945
- aws_secretsmanager_secret_version.appsync_api_key,
946
978
  ]
947
979
  # eval-runner walks every test case sequentially, invoking an agent +
948
980
  # waiting up to 2 min for spans to propagate per test, so a 10-test run
949
981
  # can easily exceed the 30 s default. 900 s covers ~5-15 min sweeps.
950
- # wiki-bootstrap-import runs a full Hindsight ingest for ~3,000 records;
951
- # the LLM-backed retain path makes it the longest-running Lambda in the
952
- # set. 900 s is Lambda's per-invocation max and matches eval-runner's ceiling.
953
982
  # routine-task-python wraps a 300s sandbox session and needs headroom
954
983
  # for the Start/Invoke/Stop/S3-offload round trip; 360s leaves ~60s
955
984
  # for AWS-call setup and offload after the sandbox's own ceiling.
@@ -957,8 +986,16 @@ resource "aws_lambda_function" "handler" {
957
986
  # validates the agent, builds the AgentCore invoke payload, dispatches
958
987
  # Event-mode, and returns. Setup is ~5s in practice; 60s gives 12×
959
988
  # headroom for transient slowness.
960
- timeout = 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
961
- memory_size = each.key == "graphql-http" ? 512 : each.key == "wakeup-processor" ? 512 : each.key == "workspace-event-dispatcher" ? 512 : each.key == "eval-runner" ? 512 : each.key == "eval-worker" ? 512 : each.key == "wiki-compile" ? 1024 : each.key == "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
989
+ # reference QBR run was ~2 min; 900s is the ceiling a longer run is a
990
+ # legitimate trial limitation, recorded, not engineered around).
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
962
999
 
963
1000
  filename = local.use_local_zips ? "${var.lambda_zips_dir}/${each.key}.zip" : null
964
1001
  source_code_hash = local.use_local_zips ? filebase64sha256("${var.lambda_zips_dir}/${each.key}.zip") : null
@@ -976,14 +1013,7 @@ resource "aws_lambda_function" "handler" {
976
1013
  # document-conformance-judge is also a single-writer: direct
977
1014
  # process-and-complete with no in-flight claim status depends on never
978
1015
  # having two sweepers race the same pending rows (THINK-189 KTD4).
979
- # analyst-query-broker is capped low: each concurrent execution holds a
980
- # dedicated analyst_reader Postgres connection and runs model-authored
981
- # SQL — the cap bounds both connection pressure and the blast radius of
982
- # a runaway delegation (THINK-228 U3).
983
- # analyst-connection-reconciler is capped at 1: the probe holds one
984
- # analyst_reader connection and overlapping probes are pointless (the
985
- # cluster-global reader has one grant surface / one live schema).
986
- 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
987
1017
 
988
1018
  environment {
989
1019
  variables = merge(
@@ -993,44 +1023,22 @@ resource "aws_lambda_function" "handler" {
993
1023
  )
994
1024
  }
995
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.
996
1029
  dynamic "vpc_config" {
997
- for_each = each.key == "okf-efs-refresh" && local.okf_efs_vpc_enabled ? [1] : []
998
-
999
- content {
1000
- subnet_ids = var.okf_efs_subnet_ids
1001
- security_group_ids = var.okf_efs_security_group_ids
1002
- }
1003
- }
1004
-
1005
- # Analyst data-path handlers get a stable NAT egress IP when
1006
- # analyst_egress_subnet_ids is set — external Postgres sources behind an IP
1007
- # allowlist admit that one EIP. Disjoint from the okf-efs-refresh block
1008
- # above (a function takes at most one vpc_config).
1009
- dynamic "vpc_config" {
1010
- for_each = contains(local.analyst_vpc_handlers, each.key) && local.analyst_vpc_enabled ? [1] : []
1011
-
1012
- content {
1013
- subnet_ids = var.analyst_egress_subnet_ids
1014
- security_group_ids = var.analyst_egress_security_group_ids
1015
- }
1016
- }
1017
-
1018
- dynamic "file_system_config" {
1019
- 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]]
1020
1031
 
1021
1032
  content {
1022
- arn = var.okf_efs_refresh_access_point_arn
1023
- 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
1024
1035
  }
1025
1036
  }
1026
1037
 
1027
1038
  lifecycle {
1028
1039
  precondition {
1029
- condition = each.key != "okf-efs-refresh" || var.okf_efs_refresh_access_point_arn == "" || (
1030
- length(var.okf_efs_mount_target_ids) == length(var.okf_efs_subnet_ids) &&
1031
- alltrue([for id in var.okf_efs_mount_target_ids : id != ""])
1032
- )
1033
- 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."
1034
1042
  }
1035
1043
  }
1036
1044
 
@@ -1041,31 +1049,59 @@ resource "aws_lambda_function" "handler" {
1041
1049
  }
1042
1050
 
1043
1051
  # ---------------------------------------------------------------------------
1044
- # 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
1045
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).
1046
1058
  #
1047
- # AWS Lambda's default async invoke retries the function 2 times with a
1048
- # 1-minute delay before sending failures to a DLQ (or dropping). For
1049
- # wiki-compile, retries duplicate Bedrock cost AND can produce duplicate
1050
- # user-visible threads + workspace_runs (the brain-enrichment draft path
1051
- # in particularsee plan 2026-05-01-002 U5/U6 and
1052
- # docs/solutions/architecture-patterns/async-retry-idempotency-lessons).
1053
- #
1054
- # Pin retries to 0 and route failures to a dedicated DLQ. The runner's
1055
- # job-status short-circuit (running/succeeded/failed/skipped) is the
1056
- # in-process protection against duplicate writebacks; this is the
1057
- # 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.
1058
1066
 
1059
- resource "aws_sqs_queue" "wiki_compile_dlq" {
1060
- count = local.deploy_lambda_handlers ? 1 : 0
1061
- name = "thinkwork-${var.stage}-wiki-compile-dlq"
1062
- message_retention_seconds = 1209600 # 14 days
1063
-
1064
- tags = {
1065
- 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
1066
1075
  }
1067
1076
  }
1068
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
+
1069
1105
  # chat-agent-invoke retry-0 (plan 2026-05-22-006 U3). After the
1070
1106
  # direct-callback finalize refactor, chat-agent-invoke is Event-mode-
1071
1107
  # dispatched by the GraphQL resolver and itself dispatches AgentCore
@@ -1084,66 +1120,41 @@ resource "aws_lambda_function_event_invoke_config" "chat_agent_invoke" {
1084
1120
  maximum_event_age_in_seconds = 3600
1085
1121
  }
1086
1122
 
1087
- 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" {
1088
1128
  count = local.deploy_lambda_handlers ? 1 : 0
1089
- function_name = aws_lambda_function.handler["wiki-compile"].function_name
1090
- maximum_retry_attempts = 0
1091
- maximum_event_age_in_seconds = 3600
1092
-
1093
- destination_config {
1094
- on_failure {
1095
- destination = aws_sqs_queue.wiki_compile_dlq[0].arn
1096
- }
1097
- }
1098
- }
1099
-
1100
- # Ontology suggestion scans are durable-job driven. Disable AWS async
1101
- # retries so duplicate scan invocations do not create duplicate review
1102
- # proposals; the scan job row is the retry/observability surface.
1103
- resource "aws_sqs_queue" "ontology_scan_dlq" {
1104
- count = local.deploy_lambda_handlers ? 1 : 0
1105
- name = "thinkwork-${var.stage}-ontology-scan-dlq"
1106
- message_retention_seconds = 1209600 # 14 days
1107
-
1108
- tags = {
1109
- Name = "thinkwork-${var.stage}-ontology-scan-dlq"
1110
- }
1111
- }
1112
-
1113
- resource "aws_lambda_function_event_invoke_config" "ontology_scan" {
1114
- count = local.deploy_lambda_handlers ? 1 : 0
1115
- 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
1116
1131
  maximum_retry_attempts = 0
1117
1132
  maximum_event_age_in_seconds = 3600
1118
-
1119
- destination_config {
1120
- on_failure {
1121
- destination = aws_sqs_queue.ontology_scan_dlq[0].arn
1122
- }
1123
- }
1124
1133
  }
1125
1134
 
1126
- # Ontology reprocess jobs are row-ledger driven and explicitly claim work.
1127
- # Disable AWS async retries to keep failure/retry state in ontology.reprocess_jobs.
1128
- 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" {
1129
1140
  count = local.deploy_lambda_handlers ? 1 : 0
1130
- name = "thinkwork-${var.stage}-ontology-reprocess-dlq"
1141
+ name = "thinkwork-${var.stage}-identity-match-dlq"
1131
1142
  message_retention_seconds = 1209600 # 14 days
1132
1143
 
1133
1144
  tags = {
1134
- Name = "thinkwork-${var.stage}-ontology-reprocess-dlq"
1145
+ Name = "thinkwork-${var.stage}-identity-match-dlq"
1135
1146
  }
1136
1147
  }
1137
1148
 
1138
- resource "aws_lambda_function_event_invoke_config" "ontology_reprocess" {
1149
+ resource "aws_lambda_function_event_invoke_config" "identity_match" {
1139
1150
  count = local.deploy_lambda_handlers ? 1 : 0
1140
- function_name = aws_lambda_function.handler["ontology-reprocess"].function_name
1151
+ function_name = aws_lambda_function.handler["identity-match"].function_name
1141
1152
  maximum_retry_attempts = 0
1142
1153
  maximum_event_age_in_seconds = 3600
1143
1154
 
1144
1155
  destination_config {
1145
1156
  on_failure {
1146
- destination = aws_sqs_queue.ontology_reprocess_dlq[0].arn
1157
+ destination = aws_sqs_queue.identity_match_dlq[0].arn
1147
1158
  }
1148
1159
  }
1149
1160
  }
@@ -1167,7 +1178,7 @@ resource "aws_lambda_function_event_invoke_config" "routine_approval_callback" {
1167
1178
  # memory-retain after every chat turn. AWS Lambda's default async-retry
1168
1179
  # policy is 2 attempts; without overriding it, a transient failure on the
1169
1180
  # canonical-transcript fetch or adapter write retries the entire writeback
1170
- # 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
1171
1182
  # longest-suffix-prefix merge in memory-retain.ts dedupes content but the
1172
1183
  # retain-cost path (Bedrock tokens charged in adapter.retainConversation)
1173
1184
  # is NOT idempotent — retries multiply LLM cost. Per
@@ -1454,8 +1465,13 @@ locals {
1454
1465
  # Public login capabilities. Unauthenticated by design; the handler
1455
1466
  # resolves tenant-scoped OAuth options only from trusted API Gateway
1456
1467
  # domain metadata and fails closed for unknown/shared hosts.
1457
- "GET /api/auth/options" = "public-auth-options"
1458
- "OPTIONS /api/auth/options" = "public-auth-options"
1468
+ "GET /api/auth/options" = "public-auth-options"
1469
+ "OPTIONS /api/auth/options" = "public-auth-options"
1470
+ # Operator/deployment-only metadata seam. The Lambda independently
1471
+ # verifies API_AUTH_SECRET and rejects raw secret values.
1472
+ "POST /api/auth/providers/reconcile" = "auth-provider-reconcile"
1473
+ # Rollback-only routes. No native client links to these, and the entire
1474
+ # handler is filtered from Lambda/API Gateway in the retired phase.
1459
1475
  "GET /api/auth/workos/authorize" = "workos-auth"
1460
1476
  "OPTIONS /api/auth/workos/authorize" = "workos-auth"
1461
1477
  "GET /api/auth/workos/callback" = "workos-auth"
@@ -1472,21 +1488,6 @@ locals {
1472
1488
  # Agent actions (start/stop/heartbeat/budget)
1473
1489
  "ANY /api/agent-actions/{proxy+}" = "agent-actions"
1474
1490
 
1475
- # Desktop-local Pi tombstones. Specific routes before broad REST handlers;
1476
- # OPTIONS is handled inside the Lambda before auth.
1477
- "POST /api/desktop/runtime-session" = "desktop-runtime-session"
1478
- "OPTIONS /api/desktop/runtime-session" = "desktop-runtime-session"
1479
- "POST /api/desktop/workspace-prewarm" = "desktop-workspace-prewarm"
1480
- "OPTIONS /api/desktop/workspace-prewarm" = "desktop-workspace-prewarm"
1481
- "POST /api/desktop/managed-delegation" = "managed-delegation"
1482
- "OPTIONS /api/desktop/managed-delegation" = "managed-delegation"
1483
- "POST /api/desktop/eval-runs" = "desktop-eval-runs"
1484
- "OPTIONS /api/desktop/eval-runs" = "desktop-eval-runs"
1485
- "POST /api/desktop/eval-runs/{runId}/sessions" = "desktop-eval-runs"
1486
- "OPTIONS /api/desktop/eval-runs/{runId}/sessions" = "desktop-eval-runs"
1487
- "POST /api/desktop/eval-runs/{runId}/results" = "desktop-eval-runs"
1488
- "OPTIONS /api/desktop/eval-runs/{runId}/results" = "desktop-eval-runs"
1489
-
1490
1491
  # Mobile agent harness model proxy (cloud Bedrock Converse). OPTIONS is
1491
1492
  # handled inside the Lambda before auth.
1492
1493
  "POST /api/model/converse" = "model-converse"
@@ -1555,14 +1556,10 @@ locals {
1555
1556
  "GET /.well-known/oauth-authorization-server" = "mcp-oauth"
1556
1557
  "GET /.well-known/openid-configuration" = "mcp-oauth"
1557
1558
  "GET /mcp/oauth/jwks" = "mcp-oauth"
1558
- "POST /mcp/oauth/register" = "mcp-oauth"
1559
- "GET /mcp/oauth/authorize" = "mcp-oauth"
1560
- "GET /mcp/oauth/callback" = "mcp-oauth"
1561
- "POST /mcp/oauth/token" = "mcp-oauth"
1562
- "POST /mcp/oauth/revoke" = "mcp-oauth"
1563
- "ANY /mcp/user-memory" = "mcp-user-memory"
1564
- "ANY /mcp/context-engine" = "mcp-context-engine"
1565
- "ANY /mcp/open-engine" = "mcp-open-engine"
1559
+ # THINK-316 U1: AgentCore CUSTOM_JWT discovery is deliberately isolated
1560
+ # under /agentcore so the existing user-memory OAuth issuer is unchanged.
1561
+ "GET /agentcore/.well-known/openid-configuration" = "mcp-oauth"
1562
+ "GET /agentcore/oauth/jwks" = "mcp-oauth"
1566
1563
  # THINK-280 U8: scoped external capability search facade. The handler is
1567
1564
  # inert unless CAPABILITY_EXTERNAL_SEARCH_ENABLED (gated on
1568
1565
  # enable_capability_broker) is on — routing it while disabled returns an
@@ -1607,6 +1604,18 @@ locals {
1607
1604
  "OPTIONS /api/deployment-sessions/{sessionId}/teardown" = "deployment-sessions"
1608
1605
  "GET /api/auth/me" = "auth-me"
1609
1606
  "OPTIONS /api/auth/me" = "auth-me"
1607
+ "POST /api/auth/revoke" = "auth-revoke"
1608
+ "OPTIONS /api/auth/revoke" = "auth-revoke"
1609
+ "POST /api/auth/subscription-ticket" = "auth-subscription-ticket"
1610
+ "OPTIONS /api/auth/subscription-ticket" = "auth-subscription-ticket"
1611
+ "POST /api/auth/enrollment/consume" = "auth-enrollment"
1612
+ "OPTIONS /api/auth/enrollment/consume" = "auth-enrollment"
1613
+ "POST /api/auth/enrollment/recover" = "auth-enrollment"
1614
+ "OPTIONS /api/auth/enrollment/recover" = "auth-enrollment"
1615
+ "POST /api/auth/enrollment/migrate" = "auth-enrollment"
1616
+ "OPTIONS /api/auth/enrollment/migrate" = "auth-enrollment"
1617
+ "POST /api/auth/enrollment/auto-link" = "auth-enrollment"
1618
+ "OPTIONS /api/auth/enrollment/auto-link" = "auth-enrollment"
1610
1619
  "ANY /api/extensions/{extensionId}" = "extension-proxy"
1611
1620
  "ANY /api/extensions/{extensionId}/{proxy+}" = "extension-proxy"
1612
1621
 
@@ -1665,9 +1674,6 @@ locals {
1665
1674
  # Workspace files
1666
1675
  "ANY /api/workspaces/{proxy+}" = "workspace-files"
1667
1676
 
1668
- # Knowledge bases
1669
- "ANY /api/knowledge-bases/{proxy+}" = "knowledge-base-files"
1670
-
1671
1677
  # Email
1672
1678
  "POST /api/email/send" = "email-send"
1673
1679
  "POST /api/email/provider-webhook/{providerInstallId}" = "email-provider-webhook"
@@ -1689,7 +1695,6 @@ locals {
1689
1695
  "POST /msteams/account-link/complete" = "msteams-account-link-complete"
1690
1696
 
1691
1697
  # Memory
1692
- "ANY /api/memory/{proxy+}" = "memory"
1693
1698
 
1694
1699
  # Artifacts
1695
1700
  "POST /api/artifacts/{proxy+}" = "artifact-deliver"
@@ -1755,20 +1760,6 @@ locals {
1755
1760
  # retained as a break-glass superuser path for bootstrap/debug.
1756
1761
  "POST /mcp/admin" = "admin-ops-mcp"
1757
1762
 
1758
- # Analyst query broker — first-party MCP server exposing query
1759
- # (THINK-228 U3). Callers present the tenant-wide broker service
1760
- # credential as Bearer; SQL executes as the hardened analyst_reader
1761
- # role. The seeded postgres-dev connector row points at this route.
1762
- "POST /mcp/analyst" = "analyst-query-broker"
1763
-
1764
- # Sourced analyst broker route (THINK-239) — the same handler serves a
1765
- # registered EXTERNAL Postgres source at /mcp/analyst/<slug>. The broker
1766
- # requires a signed caller context whose sourceClaims.slug matches the
1767
- # path (the legacy bearer is never accepted here) and connects using the
1768
- # per-source reader credential (thinkwork/<stage>/analyst/*, covered by
1769
- # the shared secretsmanager:GetSecretValue on thinkwork/*).
1770
- "POST /mcp/analyst/{sourceSlug}" = "analyst-query-broker"
1771
-
1772
1763
  # MCP admin key management — per-tenant Bearer token CRUD. Tokens
1773
1764
  # are shown ONCE at creation (POST returns raw value); server stores
1774
1765
  # sha256 hash only. These specific routes take precedence over the
@@ -1778,6 +1769,15 @@ locals {
1778
1769
  "GET /api/tenants/{tenantId}/mcp-admin-keys" = "mcp-admin-keys"
1779
1770
  "DELETE /api/tenants/{tenantId}/mcp-admin-keys/{keyId}" = "mcp-admin-keys"
1780
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
+
1781
1781
  # One-shot tenant provisioning for the admin-ops MCP. Mints a fresh
1782
1782
  # tkm_ key + stores it in Secrets Manager at
1783
1783
  # thinkwork/<stage>/mcp/<tenantId>/admin-ops + upserts the
@@ -1785,6 +1785,13 @@ locals {
1785
1785
  # any agent that gets it assigned via agent_mcp_servers.
1786
1786
  "POST /api/tenants/{tenantId}/mcp-admin-provision" = "mcp-admin-provision"
1787
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
+
1788
1795
  # MCP server admin approval (plan §U11, SI-5). Externally-sourced MCP
1789
1796
  # servers land with status='pending'; these routes flip them to
1790
1797
  # approved/rejected. Cognito JWT only (mcp-approval handler rejects
@@ -1802,6 +1809,10 @@ locals {
1802
1809
  "POST /api/threads/{threadId}/attachments/finalize" = "thread-attachments-finalize"
1803
1810
  "OPTIONS /api/threads/{threadId}/attachments/finalize" = "thread-attachments-finalize"
1804
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
+
1805
1816
  # U9-remainder of finance pilot — tenant-pinned download endpoint.
1806
1817
  "GET /api/threads/{threadId}/attachments/{attachmentId}/download" = "thread-attachment-download"
1807
1818
  "OPTIONS /api/threads/{threadId}/attachments/{attachmentId}/download" = "thread-attachment-download"
@@ -1816,12 +1827,20 @@ locals {
1816
1827
  "POST /api/runtime/manifests" = "manifest-log"
1817
1828
  "OPTIONS /api/runtime/manifests" = "manifest-log"
1818
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
+
1819
1836
  # SI-7 catalog-list read (plan §U15 pt 3/3). The runtime fetches
1820
1837
  # the allowed slug set once per session-start. Shared API_AUTH_SECRET.
1821
1838
  "GET /api/runtime/capability-catalog" = "capability-catalog-list"
1822
1839
  "OPTIONS /api/runtime/capability-catalog" = "capability-catalog-list"
1823
1840
  } : route_key => handler_name
1824
- if !contains(local.optional_integration_handler_names, handler_name)
1841
+ if !contains(local.optional_integration_handler_names, handler_name) && (
1842
+ !startswith(route_key, "GET /agentcore/")
1843
+ )
1825
1844
  } : {}
1826
1845
  }
1827
1846
 
@@ -1842,6 +1861,16 @@ resource "aws_apigatewayv2_route" "handler" {
1842
1861
  target = "integrations/${aws_apigatewayv2_integration.handler[each.key].id}"
1843
1862
  }
1844
1863
 
1864
+ resource "aws_lambda_permission" "appsync_subscription_authorizer" {
1865
+ count = local.deploy_lambda_handlers ? 1 : 0
1866
+
1867
+ statement_id = "AllowAppSyncSubscriptionAuthorization"
1868
+ action = "lambda:InvokeFunction"
1869
+ function_name = aws_lambda_function.handler["appsync-subscription-authorizer"].function_name
1870
+ principal = "appsync.amazonaws.com"
1871
+ source_arn = "arn:aws:appsync:${var.region}:${var.account_id}:apis/${var.appsync_api_id}"
1872
+ }
1873
+
1845
1874
  resource "aws_lambda_permission" "handler_apigw" {
1846
1875
  for_each = local.deploy_lambda_handlers ? toset(distinct(values(local.api_routes))) : toset([])
1847
1876
 
@@ -1917,16 +1946,15 @@ resource "aws_scheduler_schedule" "wakeup_processor" {
1917
1946
  }
1918
1947
  }
1919
1948
 
1920
- # ---------------------------------------------------------------------------
1921
- # webhook_deliveries retention cron daily delete of rows older than 90 days
1922
- # ---------------------------------------------------------------------------
1923
-
1924
- resource "aws_scheduler_schedule" "webhook_deliveries_cleanup" {
1949
+ # Replays durable authorization-revocation invalidations. The worker owns its
1950
+ # retry state in Aurora and suppresses tenant realtime fan-out until AppSync
1951
+ # confirms every sensitive subscription class has been invalidated.
1952
+ resource "aws_scheduler_schedule" "subscription_invalidation" {
1925
1953
  count = local.deploy_lambda_handlers ? 1 : 0
1926
1954
 
1927
- name = "thinkwork-${var.stage}-webhook-deliveries-cleanup"
1955
+ name = "thinkwork-${var.stage}-subscription-invalidation"
1928
1956
  group_name = "default"
1929
- schedule_expression = "cron(0 4 * * ? *)" # daily at 04:00 UTC
1957
+ schedule_expression = "rate(1 minutes)"
1930
1958
  state = "ENABLED"
1931
1959
 
1932
1960
  flexible_time_window {
@@ -1934,51 +1962,42 @@ resource "aws_scheduler_schedule" "webhook_deliveries_cleanup" {
1934
1962
  }
1935
1963
 
1936
1964
  target {
1937
- arn = aws_lambda_function.handler["webhook-deliveries-cleanup"].arn
1965
+ arn = aws_lambda_function.handler["subscription-invalidation"].arn
1938
1966
  role_arn = aws_iam_role.scheduler.arn
1967
+ input = jsonencode({ limit = 25 })
1968
+
1969
+ retry_policy {
1970
+ maximum_retry_attempts = 0
1971
+ }
1939
1972
  }
1940
1973
  }
1941
1974
 
1942
1975
  # ---------------------------------------------------------------------------
1943
- # Requester memory dreamingbroad per-user memory compaction/reflection sweep
1944
- # ---------------------------------------------------------------------------
1945
-
1946
- # ---------------------------------------------------------------------------
1947
- # Brain dream state — per-bank Hindsight consolidation with audit ledger
1948
- # (THINK-133 U4). Retries are the ledger's job (staged plan -> atomic apply
1949
- # -> applied markers; unfinished runs resume on the next tick), so Lambda
1950
- # async retries stay at 0, mirroring memory-retain.
1976
+ # webhook_deliveries retention crondaily delete of rows older than 90 days
1951
1977
  # ---------------------------------------------------------------------------
1952
1978
 
1953
- resource "aws_lambda_function_event_invoke_config" "brain_dream_state" {
1954
- count = local.deploy_lambda_handlers ? 1 : 0
1955
- function_name = aws_lambda_function.handler["brain-dream-state"].function_name
1956
- maximum_retry_attempts = 0
1957
- maximum_event_age_in_seconds = 3600
1958
- }
1959
-
1960
- resource "aws_scheduler_schedule" "brain_dream_state" {
1979
+ resource "aws_scheduler_schedule" "webhook_deliveries_cleanup" {
1961
1980
  count = local.deploy_lambda_handlers ? 1 : 0
1962
1981
 
1963
- name = "thinkwork-${var.stage}-brain-dream-state"
1982
+ name = "thinkwork-${var.stage}-webhook-deliveries-cleanup"
1964
1983
  group_name = "default"
1965
- schedule_expression = var.brain_dream_state_schedule_expression
1966
- state = var.brain_dream_state_enabled ? "ENABLED" : "DISABLED"
1984
+ schedule_expression = "cron(0 4 * * ? *)" # daily at 04:00 UTC
1985
+ state = "ENABLED"
1967
1986
 
1968
1987
  flexible_time_window {
1969
1988
  mode = "OFF"
1970
1989
  }
1971
1990
 
1972
1991
  target {
1973
- arn = aws_lambda_function.handler["brain-dream-state"].arn
1992
+ arn = aws_lambda_function.handler["webhook-deliveries-cleanup"].arn
1974
1993
  role_arn = aws_iam_role.scheduler.arn
1975
-
1976
- retry_policy {
1977
- maximum_retry_attempts = 0
1978
- }
1979
1994
  }
1980
1995
  }
1981
1996
 
1997
+ # ---------------------------------------------------------------------------
1998
+ # Requester memory dreaming — broad per-user memory compaction/reflection sweep
1999
+ # ---------------------------------------------------------------------------
2000
+
1982
2001
  resource "aws_scheduler_schedule" "requester_memory_dreaming" {
1983
2002
  count = local.deploy_lambda_handlers ? 1 : 0
1984
2003
 
@@ -2041,20 +2060,12 @@ resource "aws_scheduler_schedule" "mcp_approval_sweeper" {
2041
2060
  }
2042
2061
  }
2043
2062
 
2044
- # ---------------------------------------------------------------------------
2045
- # skill_runs reconciler — transitions stuck-running rows to failed every 5 min.
2046
- # Guards against agentcore Lambda crashes / OOMs that drop the
2047
- # /api/skills/complete writeback and leave the row at 'running' forever,
2048
- # which in turn blocks the dedup partial unique index from letting retries
2049
- # through.
2050
- # ---------------------------------------------------------------------------
2051
-
2052
- resource "aws_scheduler_schedule" "skill_runs_reconciler" {
2063
+ resource "aws_scheduler_schedule" "inbox_approval_sweeper" {
2053
2064
  count = local.deploy_lambda_handlers ? 1 : 0
2054
2065
 
2055
- name = "thinkwork-${var.stage}-skill-runs-reconciler"
2066
+ name = "thinkwork-${var.stage}-inbox-approval-sweeper"
2056
2067
  group_name = "default"
2057
- schedule_expression = "rate(5 minutes)"
2068
+ schedule_expression = "cron(30 4 * * ? *)" # daily at 04:30 UTC (offset from mcp-approval-sweeper)
2058
2069
  state = "ENABLED"
2059
2070
 
2060
2071
  flexible_time_window {
@@ -2062,53 +2073,50 @@ resource "aws_scheduler_schedule" "skill_runs_reconciler" {
2062
2073
  }
2063
2074
 
2064
2075
  target {
2065
- arn = aws_lambda_function.handler["skill-runs-reconciler"].arn
2076
+ arn = aws_lambda_function.handler["inbox-approval-sweeper"].arn
2066
2077
  role_arn = aws_iam_role.scheduler.arn
2067
2078
  }
2068
2079
  }
2069
2080
 
2070
- # ---------------------------------------------------------------------------
2071
- # analyst_connection_reconciler probes the analyst_reader connection every
2072
- # 30 min (reachability, IAM auth, SELECT-grant introspection, zero-write
2073
- # assertion, schema-drift hash; all read-only) and stamps the verdict onto
2074
- # tenant_mcp_servers.runtime_metadata.analyst_probe. Dispatch withholds a
2075
- # failing/stale connection loudly (THINK-229 U5, R7/R8, KTD8).
2076
- #
2077
- # retry-0 + DLQ (project_async_retry_idempotency_lessons): the probe is
2078
- # cheap and idempotent the next 30-minute tick IS the retry, so stacking
2079
- # scheduler retries only adds redundant connections. A failure lands in the
2080
- # DLQ for operator visibility instead.
2081
- # ---------------------------------------------------------------------------
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
2082
2090
 
2083
- resource "aws_sqs_queue" "analyst_connection_reconciler_dlq" {
2084
- count = local.deploy_lambda_handlers ? 1 : 0
2085
- name = "thinkwork-${var.stage}-analyst-connection-reconciler-dlq"
2086
- 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"
2087
2095
 
2088
- tags = {
2089
- Name = "thinkwork-${var.stage}-analyst-connection-reconciler-dlq"
2096
+ flexible_time_window {
2097
+ mode = "OFF"
2090
2098
  }
2091
- }
2092
2099
 
2093
- resource "aws_lambda_function_event_invoke_config" "analyst_connection_reconciler" {
2094
- count = local.deploy_lambda_handlers ? 1 : 0
2095
- function_name = aws_lambda_function.handler["analyst-connection-reconciler"].function_name
2096
- maximum_retry_attempts = 0
2097
- maximum_event_age_in_seconds = 3600
2098
-
2099
- destination_config {
2100
- on_failure {
2101
- destination = aws_sqs_queue.analyst_connection_reconciler_dlq[0].arn
2102
- }
2100
+ target {
2101
+ arn = aws_lambda_function.handler["auth-ticket-sweeper"].arn
2102
+ role_arn = aws_iam_role.scheduler.arn
2103
2103
  }
2104
2104
  }
2105
2105
 
2106
- 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" {
2107
2115
  count = local.deploy_lambda_handlers ? 1 : 0
2108
2116
 
2109
- name = "thinkwork-${var.stage}-analyst-connection-reconciler"
2117
+ name = "thinkwork-${var.stage}-skill-runs-reconciler"
2110
2118
  group_name = "default"
2111
- schedule_expression = "rate(30 minutes)"
2119
+ schedule_expression = "rate(5 minutes)"
2112
2120
  state = "ENABLED"
2113
2121
 
2114
2122
  flexible_time_window {
@@ -2116,12 +2124,8 @@ resource "aws_scheduler_schedule" "analyst_connection_reconciler" {
2116
2124
  }
2117
2125
 
2118
2126
  target {
2119
- arn = aws_lambda_function.handler["analyst-connection-reconciler"].arn
2127
+ arn = aws_lambda_function.handler["skill-runs-reconciler"].arn
2120
2128
  role_arn = aws_iam_role.scheduler.arn
2121
-
2122
- retry_policy {
2123
- maximum_retry_attempts = 0
2124
- }
2125
2129
  }
2126
2130
  }
2127
2131
 
@@ -2482,175 +2486,44 @@ resource "aws_scheduler_schedule" "stall_monitor" {
2482
2486
  }
2483
2487
  }
2484
2488
 
2485
- # ---------------------------------------------------------------------------
2486
- # Retry dispatcher — drains retry_queue rows the stall monitor enqueues and
2487
- # re-runs genuinely stalled turns (THINK-307). First-ever scheduling of this
2488
- # handler; state is var-driven and defaults DISABLED — never enable on a
2489
- # stage before THINK-305/308/309 are live there (parent plan deploy ordering).
2490
- # The 1-minute rate is fixed by design: the queue's own scheduled_at does the
2491
- # pacing. Scheduler retries stay at 0 — the next 1-minute tick IS the retry.
2492
- # ---------------------------------------------------------------------------
2493
-
2494
- # Lambda async retries are disabled (mirrors memory-retraction-drainer): the
2495
- # claim is a CAS UPDATE so a duplicate invocation only picks up new rows, and
2496
- # the next 1-minute tick is the retry.
2497
- resource "aws_lambda_function_event_invoke_config" "retry_dispatcher" {
2498
- count = local.deploy_lambda_handlers ? 1 : 0
2499
- function_name = aws_lambda_function.handler["cron-retry-dispatcher"].function_name
2500
- maximum_retry_attempts = 0
2501
- maximum_event_age_in_seconds = 3600
2502
- }
2503
-
2504
- resource "aws_scheduler_schedule" "retry_dispatcher" {
2505
- count = local.deploy_lambda_handlers ? 1 : 0
2506
-
2507
- name = "thinkwork-${var.stage}-retry-dispatcher"
2508
- group_name = "default"
2509
- schedule_expression = "rate(1 minutes)"
2510
- state = var.retry_dispatcher_enabled ? "ENABLED" : "DISABLED"
2511
-
2512
- flexible_time_window {
2513
- mode = "OFF"
2514
- }
2515
-
2516
- target {
2517
- arn = aws_lambda_function.handler["cron-retry-dispatcher"].arn
2518
- role_arn = aws_iam_role.scheduler.arn
2519
-
2520
- retry_policy {
2521
- maximum_retry_attempts = 0
2522
- }
2523
- }
2524
- }
2525
-
2526
2489
 
2527
2490
  # ---------------------------------------------------------------------------
2528
2491
  # Compounding Memory — nightly hygiene + export
2529
2492
  # ---------------------------------------------------------------------------
2530
2493
 
2531
- resource "aws_scheduler_schedule" "wiki_compile_drainer" {
2532
- count = local.deploy_lambda_handlers ? 1 : 0
2533
-
2534
- name = "thinkwork-${var.stage}-wiki-compile-drainer"
2535
- group_name = "default"
2536
- schedule_expression = "rate(1 minutes)"
2537
- state = "ENABLED"
2538
-
2539
- flexible_time_window {
2540
- mode = "OFF"
2541
- }
2542
-
2543
- target {
2544
- arn = aws_lambda_function.handler["wiki-compile"].arn
2545
- role_arn = aws_iam_role.scheduler.arn
2546
- }
2547
- }
2548
-
2549
- # Observations → Knowledge Graph sweep (plan 2026-06-09-004 U5). Enumerates
2550
- # tenants and runs an incremental observations ingest per tenant; the stable
2551
- # source_ref's active-run dedupe drops overlap with operator-started runs and
2552
- # the in-handler stale-run reaper clears stranded rows past the run ceiling.
2553
- resource "aws_scheduler_schedule" "knowledge_graph_observations_ingest" {
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" {
2554
2501
  count = local.deploy_lambda_handlers ? 1 : 0
2555
2502
 
2556
- name = "thinkwork-${var.stage}-knowledge-graph-observations-ingest"
2503
+ name = "thinkwork-${var.stage}-identity-drift-match"
2557
2504
  group_name = "default"
2558
- schedule_expression = "rate(30 minutes)"
2559
- # Ships DISABLED (plan 2026-07-03-005 U4/KTD-6): the schedule enables on dev
2560
- # only after a manual golden-set-validated run. Driven by the GHA
2561
- # WIKI_KG_INGEST_ENABLED var.
2562
- state = var.knowledge_graph_observations_ingest_enabled ? "ENABLED" : "DISABLED"
2505
+ schedule_expression = "rate(1 days)"
2506
+ state = var.identity_drift_match_enabled ? "ENABLED" : "DISABLED"
2563
2507
 
2564
2508
  flexible_time_window {
2565
2509
  mode = "OFF"
2566
2510
  }
2567
2511
 
2568
2512
  target {
2569
- arn = aws_lambda_function.handler["knowledge-graph-observations-ingest"].arn
2513
+ arn = aws_lambda_function.handler["identity-match"].arn
2570
2514
  role_arn = aws_iam_role.scheduler.arn
2571
- input = jsonencode({ sweep = true, trigger = "scheduled" })
2515
+ input = jsonencode({ drift = true, trigger = "scheduled" })
2572
2516
 
2573
- # Periodic idempotent worker: the next 30-minute tick IS the retry.
2574
- # Scheduler-level retries stack extra invocations onto a cadence that
2575
- # already self-corrects (cursor-guarded), compounding the invocation
2576
- # volume that helped trip Lambda recursive-loop detection.
2517
+ # Periodic idempotent worker: the next daily tick IS the retry
2518
+ # scheduler retries only stack invocations onto a self-correcting
2519
+ # cadence.
2577
2520
  retry_policy {
2578
2521
  maximum_retry_attempts = 0
2579
2522
  }
2580
2523
  }
2581
2524
  }
2582
2525
 
2583
- resource "aws_scheduler_schedule" "wiki_lint" {
2584
- count = local.deploy_lambda_handlers ? 1 : 0
2585
-
2586
- name = "thinkwork-${var.stage}-wiki-lint"
2587
- group_name = "default"
2588
- schedule_expression = "cron(0 2 * * ? *)" # daily at 02:00 UTC
2589
- state = "ENABLED"
2590
-
2591
- flexible_time_window {
2592
- mode = "OFF"
2593
- }
2594
-
2595
- target {
2596
- arn = aws_lambda_function.handler["wiki-lint"].arn
2597
- role_arn = aws_iam_role.scheduler.arn
2598
- }
2599
- }
2600
-
2601
- resource "aws_scheduler_schedule" "wiki_export" {
2602
- count = local.deploy_lambda_handlers ? 1 : 0
2603
-
2604
- name = "thinkwork-${var.stage}-wiki-export"
2605
- group_name = "default"
2606
- schedule_expression = "cron(0 3 * * ? *)" # daily at 03:00 UTC (after lint)
2607
- state = "ENABLED"
2608
-
2609
- flexible_time_window {
2610
- mode = "OFF"
2611
- }
2612
-
2613
- target {
2614
- arn = aws_lambda_function.handler["wiki-export"].arn
2615
- role_arn = aws_iam_role.scheduler.arn
2616
- }
2617
- }
2618
-
2619
- # S3 bucket for markdown vault exports. One bundle per (tenant, owner, date).
2620
- # Retention is handled by the lifecycle rule below (30 days).
2621
- resource "aws_s3_bucket" "wiki_exports" {
2622
- bucket = "thinkwork-${var.stage}-wiki-exports"
2623
- force_destroy = var.stage == "dev"
2624
-
2625
- tags = {
2626
- Name = "thinkwork-${var.stage}-wiki-exports"
2627
- }
2628
- }
2629
-
2630
- resource "aws_s3_bucket_public_access_block" "wiki_exports" {
2631
- bucket = aws_s3_bucket.wiki_exports.id
2632
- block_public_acls = true
2633
- block_public_policy = true
2634
- ignore_public_acls = true
2635
- restrict_public_buckets = true
2636
- }
2637
-
2638
- resource "aws_s3_bucket_lifecycle_configuration" "wiki_exports" {
2639
- bucket = aws_s3_bucket.wiki_exports.id
2640
-
2641
- rule {
2642
- id = "expire-old-bundles"
2643
- status = "Enabled"
2644
-
2645
- filter {}
2646
-
2647
- expiration {
2648
- days = 30
2649
- }
2650
- }
2651
- }
2652
-
2653
- # Canonical ThinkWork Brain artifact store. Unlike wiki_exports, this bucket
2526
+ # Canonical ThinkWork Brain artifact store. This bucket
2654
2527
  # is the durable replay/projection substrate for Brain source artifacts,
2655
2528
  # ingestion manifests, migration snapshots, vault projections, and exports.
2656
2529
  resource "aws_s3_bucket" "brain_artifacts" {
@@ -2773,7 +2646,7 @@ resource "aws_s3_bucket_lifecycle_configuration" "brain_artifacts" {
2773
2646
 
2774
2647
  # THINK-193 U1 (Codex F6): normalized evidence snapshots are short-lived
2775
2648
  # working copies — the durable record is the evidence row (hash + ref) and
2776
- # the Hindsight projection. Expire content after 30 days; the app mirrors
2649
+ # the memory projection. Expire content after 30 days; the app mirrors
2777
2650
  # this via memory_evidence_items.snapshot_expires_at.
2778
2651
  rule {
2779
2652
  id = "expire-evidence-snapshots"
@@ -2813,6 +2686,48 @@ resource "aws_s3_bucket_lifecycle_configuration" "brain_artifacts" {
2813
2686
  noncurrent_days = 365
2814
2687
  }
2815
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
+ }
2816
2731
  }
2817
2732
 
2818
2733
  data "aws_iam_policy_document" "brain_artifacts_bucket" {
@@ -2897,33 +2812,6 @@ resource "aws_iam_role_policy" "scheduler_invoke" {
2897
2812
  # SSM Parameters — Lambda ARNs for cross-function invocation
2898
2813
  # ---------------------------------------------------------------------------
2899
2814
 
2900
- ########################################################################
2901
- # SecureString parameter for the Google Places API key. wiki-compile reads
2902
- # this on cold start via loadGooglePlacesClientFromSsm() and caches the
2903
- # client at module scope. When google_places_api_key is empty (the
2904
- # default), we seed the parameter with a placeholder so the Lambda init
2905
- # path can distinguish "unconfigured" (skip Google entirely, degrade
2906
- # gracefully) from "configured but wrong" (log + skip). lifecycle.ignore_
2907
- # changes on `value` lets ops rotate via
2908
- # aws ssm put-parameter --overwrite \
2909
- # --name /thinkwork/<stage>/google-places/api-key \
2910
- # --type SecureString --value <KEY>
2911
- # without terraform fighting it on the next apply.
2912
- ########################################################################
2913
-
2914
- resource "aws_ssm_parameter" "google_places_api_key" {
2915
- name = "/thinkwork/${var.stage}/google-places/api-key"
2916
- type = "SecureString"
2917
- value = var.google_places_api_key != "" ? var.google_places_api_key : "PLACEHOLDER_SET_VIA_CLI"
2918
- 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."
2919
-
2920
- lifecycle {
2921
- # Allow `aws ssm put-parameter --overwrite` to stick across applies.
2922
- # New-key rotation or initial population by ops should happen via CLI,
2923
- # not via terraform var.
2924
- ignore_changes = [value]
2925
- }
2926
- }
2927
2815
 
2928
2816
  ########################################################################
2929
2817
  # SecureString parameter for the graphql-http Lambda's OWN Cloudflare
@@ -2967,8 +2855,10 @@ resource "aws_ssm_parameter" "cloudflare_namespace_token" {
2967
2855
 
2968
2856
  resource "aws_ssm_parameter" "lambda_arns" {
2969
2857
  for_each = local.deploy_lambda_handlers ? {
2970
- "chat-agent-invoke-fn-arn" = aws_lambda_function.handler["chat-agent-invoke"].arn
2971
- "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
2972
2862
  "job-schedule-manager-fn-arn" = aws_lambda_function.handler["job-schedule-manager"].arn
2973
2863
  "memory-retain-fn-arn" = aws_lambda_function.handler["memory-retain"].arn
2974
2864
  "eval-runner-fn-arn" = aws_lambda_function.handler["eval-runner"].arn
@@ -3027,6 +2917,17 @@ resource "aws_lambda_function" "compliance_anchor" {
3027
2917
  COMPLIANCE_ANCHOR_OBJECT_LOCK_MODE = var.compliance_anchor_object_lock_mode
3028
2918
  }
3029
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
+ }
3030
2931
  }
3031
2932
 
3032
2933
  resource "aws_sqs_queue" "compliance_anchor_dlq" {
@@ -3333,6 +3234,17 @@ resource "aws_lambda_function" "compliance_export_runner" {
3333
3234
  DATABASE_NAME = var.database_name
3334
3235
  }
3335
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
+ }
3336
3248
  }
3337
3249
 
3338
3250
  # SQS → Lambda event source mapping. batch_size=1 so each export is a