thinkwork-cli 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/{api-client-JUBCQQDF.js → api-client-JYZXYQJR.js} +3 -1
  2. package/dist/bowser-6Z6JFAIR.js +2822 -0
  3. package/dist/chunk-DV3MRBR5.js +43 -0
  4. package/dist/chunk-DVPBI5HM.js +1728 -0
  5. package/dist/chunk-EI5ZX7CN.js +522 -0
  6. package/dist/chunk-EUJ7X6YD.js +126 -0
  7. package/dist/chunk-GG5WKBOX.js +436 -0
  8. package/dist/chunk-I64EZSU5.js +358 -0
  9. package/dist/chunk-ILZKG5WV.js +6151 -0
  10. package/dist/chunk-KMIJ5KGT.js +128 -0
  11. package/dist/chunk-OCPHU34C.js +996 -0
  12. package/dist/chunk-QOERT44A.js +3286 -0
  13. package/dist/chunk-XYM7PE2A.js +2264 -0
  14. package/dist/{chunk-STY44FQO.js → chunk-Z2F6RJFR.js} +12 -18
  15. package/dist/cli.js +72564 -18884
  16. package/dist/dist-es-CRV46D5B.js +315 -0
  17. package/dist/dist-es-DCY4IC3Y.js +2000 -0
  18. package/dist/dist-es-ENBUC3JP.js +14726 -0
  19. package/dist/dist-es-G54LBMRN.js +48 -0
  20. package/dist/dist-es-G7PTG75M.js +176 -0
  21. package/dist/dist-es-GKDBE7E6.js +88 -0
  22. package/dist/dist-es-HY3535OI.js +70 -0
  23. package/dist/dist-es-KL7Y5NB2.js +377 -0
  24. package/dist/dist-es-LDE437AM.js +170 -0
  25. package/dist/dist-es-P26FWFLT.js +485 -0
  26. package/dist/dist-es-Q57SZXZ7.js +70 -0
  27. package/dist/dist-es-R7FGL2IB.js +88 -0
  28. package/dist/dist-es-USORBXZ5.js +318 -0
  29. package/dist/dist-es-ZKC2QKD2.js +377 -0
  30. package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
  31. package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
  32. package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
  33. package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
  34. package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
  35. package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
  36. package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
  37. package/dist/drizzle/0227_analyst_reader_role.sql +106 -38
  38. package/dist/drizzle/0230_analyst_rls.sql +192 -66
  39. package/dist/drizzle/0232_memory_source_thin_ledger.sql +176 -0
  40. package/dist/drizzle/0232_message_source_event_id.sql +9 -0
  41. package/dist/drizzle/0233_msteams_install_and_links.sql +151 -0
  42. package/dist/drizzle/0233_workflow_memory_stage_tokens.sql +20 -0
  43. package/dist/drizzle/0234_memory_claim_lifecycle.sql +160 -0
  44. package/dist/drizzle/0235_workflow_token_execution_claim.sql +34 -0
  45. package/dist/drizzle/0236_memory_retraction_fencing.sql +50 -0
  46. package/dist/drizzle/0237_memory_claims_active_value_uidx.sql +172 -0
  47. package/dist/drizzle/0238_memory_erase_generation_fence.sql +97 -0
  48. package/dist/drizzle/0239_canonical_entity_identity.sql +257 -0
  49. package/dist/drizzle/0240_memory_personal_scope.sql +45 -0
  50. package/dist/drizzle/0241_knowledge_base_document_manifest.sql +67 -0
  51. package/dist/drizzle/0242_search_queries.sql +28 -0
  52. package/dist/drizzle/0243_section_sources_thread_refs.sql +12 -0
  53. package/dist/drizzle/0244_system_automations_and_memory_stage_overrides.sql +45 -0
  54. package/dist/drizzle/0245_wiki_page_render.sql +21 -0
  55. package/dist/drizzle/0246_automation_workflow_ownership.sql +30 -0
  56. package/dist/drizzle/0247_capability_runtime.sql +402 -0
  57. package/dist/drizzle/0248_capability_private_interpreter.sql +13 -0
  58. package/dist/drizzle/0249_capability_headless_execution_evidence.sql +40 -0
  59. package/dist/drizzle/0250_kg_schema_extraction.sql +576 -0
  60. package/dist/drizzle/0251_agents_json_render_ui.sql +25 -0
  61. package/dist/drizzle/0251_brain_stragglers.sql +263 -0
  62. package/dist/drizzle/0252_drop_kg_and_straggler_compat_views.sql +100 -0
  63. package/dist/drizzle/0253_drop_brain_substrate.sql +93 -0
  64. package/dist/drizzle/0254_capability_autonomous_admission.sql +29 -0
  65. package/dist/drizzle/0255_capability_autonomous_approval_mode.sql +16 -0
  66. package/dist/drizzle/0256_agent_profile_manifest_authority.sql +18 -0
  67. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  68. package/dist/drizzle/0257_capability_approvals.sql +53 -0
  69. package/dist/drizzle/0258_pending_tool_approvals.sql +84 -0
  70. package/dist/drizzle/0259_tenant_capability_registry_trust.sql +19 -0
  71. package/dist/drizzle/0260_scheduled_jobs_run_as_user.sql +20 -0
  72. package/dist/drizzle/0261_managed_harness_multiplayer.sql +313 -0
  73. package/dist/drizzle/0261_native_auth_control_plane.sql +328 -0
  74. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  75. package/dist/drizzle/0262_parallel_harness_threads.sql +9 -0
  76. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  77. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  78. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  79. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +113 -0
  80. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  81. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  82. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  83. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  84. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  85. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  86. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  87. package/dist/event-streams-7JNLJEEL.js +894 -0
  88. package/dist/event-streams-KDF5RWR6.js +1515 -0
  89. package/dist/loadSso-DJVB7BXE.js +576 -0
  90. package/dist/loadSso-JAMNA3WY.js +583 -0
  91. package/dist/plugins/catalog/package.json +1 -2
  92. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +0 -1
  93. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +0 -69
  94. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +0 -2
  95. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -5
  96. package/dist/plugins/catalog/src/contracts.ts +2 -179
  97. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -6
  98. package/dist/signin-UGDWTGS4.js +821 -0
  99. package/dist/sso-oidc-Q4VXANXY.js +837 -0
  100. package/dist/sts-CWLSE3GH.js +1241 -0
  101. package/dist/sts-DNZLNVOV.js +1245 -0
  102. package/dist/terraform/examples/greenfield/main.tf +247 -50
  103. package/dist/terraform/examples/greenfield/terraform.tfvars.example +15 -1
  104. package/dist/terraform/modules/app/agentcore-admin/main.tf +341 -0
  105. package/dist/terraform/modules/app/agentcore-admin/outputs.tf +20 -0
  106. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +61 -0
  107. package/dist/terraform/modules/app/agentcore-gateway/main.tf +305 -0
  108. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +44 -0
  109. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +93 -0
  110. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +33 -0
  111. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +543 -0
  112. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +70 -0
  113. package/dist/terraform/modules/app/agentcore-harness/main.tf +330 -0
  114. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +72 -0
  115. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +5 -0
  116. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +430 -0
  117. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +180 -0
  118. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +193 -0
  119. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +5 -0
  120. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +5 -0
  121. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +5 -0
  122. package/dist/terraform/modules/app/agentcore-harness/variables.tf +96 -0
  123. package/dist/terraform/modules/app/agentcore-identity/main.tf +173 -0
  124. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  125. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +122 -0
  126. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +25 -0
  127. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  128. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +40 -0
  129. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  130. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +74 -0
  131. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +61 -0
  132. package/dist/terraform/modules/app/agentcore-identity/variables.tf +54 -0
  133. package/dist/terraform/modules/app/agentcore-pi/main.tf +7 -0
  134. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  135. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  136. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  137. package/dist/terraform/modules/app/capability-broker/main.tf +393 -0
  138. package/dist/terraform/modules/app/capability-broker/outputs.tf +54 -0
  139. package/dist/terraform/modules/app/capability-broker/variables.tf +98 -0
  140. package/dist/terraform/modules/app/deployment-control-plane/runner.py +1011 -31
  141. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +655 -2
  142. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +88 -0
  143. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +91 -0
  144. package/dist/terraform/modules/app/lambda-api/capability-search-alarms.tf +60 -0
  145. package/dist/terraform/modules/app/lambda-api/handlers.tf +606 -37
  146. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +192 -21
  147. package/dist/terraform/modules/app/lambda-api/main.tf +35 -0
  148. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +181 -4
  149. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +92 -0
  150. package/dist/terraform/modules/app/lambda-api/msteams-app-secrets.tf +46 -0
  151. package/dist/terraform/modules/app/lambda-api/outputs.tf +35 -0
  152. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  153. package/dist/terraform/modules/app/lambda-api/variables.tf +188 -3
  154. package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/alarms.tf +47 -0
  155. package/dist/terraform/modules/app/workflow-interpreter-stepfunctions/main.tf +43 -2
  156. package/dist/terraform/modules/foundation/cognito/main.tf +319 -73
  157. package/dist/terraform/modules/foundation/cognito/outputs.tf +42 -0
  158. package/dist/terraform/modules/foundation/cognito/variables.tf +124 -2
  159. package/dist/terraform/modules/foundation/vpc/outputs.tf +5 -0
  160. package/dist/terraform/modules/thinkwork/main.tf +396 -26
  161. package/dist/terraform/modules/thinkwork/outputs.tf +188 -5
  162. package/dist/terraform/modules/thinkwork/variables.tf +154 -5
  163. package/dist/terraform/schema.graphql +69 -36
  164. package/dist/workspace-defaults/files/TOOLS.md +39 -0
  165. package/dist/workspace-defaults/files/USER.md +1 -0
  166. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +18 -0
  167. package/dist/workspace-defaults/files/agents/coding/INSTRUCTIONS.md +11 -0
  168. package/dist/workspace-defaults/files/agents/research/INSTRUCTIONS.md +12 -0
  169. package/dist/workspace-defaults/files/agents/reviewer/INSTRUCTIONS.md +15 -0
  170. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +6 -2
  171. package/package.json +6 -3
  172. package/dist/plugins/workos-auth/package.json +0 -22
  173. package/dist/plugins/workos-auth/src/index.ts +0 -30
  174. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  175. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  176. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  177. package/dist/plugins/workos-auth/tsconfig.json +0 -8
  178. /package/dist/workspace-defaults/files/{AGENTS.md → INSTRUCTIONS.md} +0 -0
@@ -8,6 +8,7 @@ import subprocess
8
8
  import tarfile
9
9
  from hashlib import sha256
10
10
  from pathlib import Path
11
+ from types import SimpleNamespace
11
12
 
12
13
  import pytest
13
14
 
@@ -94,6 +95,376 @@ def write_drizzle_files(source_dir: Path, names: list[str]) -> None:
94
95
  (migrations / name).write_text(f"-- {name}\n", encoding="utf-8")
95
96
 
96
97
 
98
+ def test_customer_update_requires_explicit_agentcore_pi_source_image() -> None:
99
+ runner = load_runner()
100
+
101
+ with pytest.raises(RuntimeError, match="agentcorePiSourceImageUri"):
102
+ runner.resolve_agentcore_pi_source_image_uri(
103
+ {
104
+ "action": "update",
105
+ "operation": {"kind": "foundation", "action": "update"},
106
+ }
107
+ )
108
+
109
+
110
+ def test_customer_update_rejects_non_customer_agentcore_pi_source_image() -> None:
111
+ runner = load_runner()
112
+
113
+ with pytest.raises(RuntimeError, match="customer-owned ECR"):
114
+ runner.resolve_agentcore_pi_source_image_uri(
115
+ {
116
+ "action": "update",
117
+ "awsAccountId": "637423202447",
118
+ "awsRegion": "us-east-1",
119
+ "agentcorePiSourceImageUri": (
120
+ "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
121
+ ),
122
+ }
123
+ )
124
+
125
+
126
+ def test_customer_update_accepts_customer_ecr_agentcore_pi_source_image() -> None:
127
+ runner = load_runner()
128
+ image = (
129
+ "637423202447.dkr.ecr.us-east-1.amazonaws.com/thinkwork-tei-e2e-agentcore:pinned@sha256:abc"
130
+ )
131
+
132
+ assert (
133
+ runner.resolve_agentcore_pi_source_image_uri(
134
+ {
135
+ "action": "update",
136
+ "awsAccountId": "637423202447",
137
+ "awsRegion": "us-east-1",
138
+ "agentcorePiSourceImageUri": image,
139
+ }
140
+ )
141
+ == image
142
+ )
143
+
144
+
145
+ def test_native_auth_reconciliation_builds_route_specific_secret_free_manifest(
146
+ monkeypatch: pytest.MonkeyPatch,
147
+ ) -> None:
148
+ runner = load_runner()
149
+ outputs = {
150
+ "user_pool_id": {"value": "us-east-1_Example123"},
151
+ "auth_route_clients": {
152
+ "value": {
153
+ "web:local": {
154
+ "client_id": "1234567890abcdefghijklmnop",
155
+ "route_key": "local",
156
+ "client_family": "web",
157
+ "provider_names": ["COGNITO"],
158
+ "explicit_auth_flows": [
159
+ "ALLOW_USER_PASSWORD_AUTH",
160
+ "ALLOW_USER_SRP_AUTH",
161
+ "ALLOW_REFRESH_TOKEN_AUTH",
162
+ ],
163
+ "callback_urls": ["https://app.example/auth/callback"],
164
+ "logout_urls": ["https://app.example"],
165
+ "lifecycle_state": "native",
166
+ },
167
+ "web:google": {
168
+ "client_id": "abcdefghijklmnopqrstuvwxyz",
169
+ "route_key": "google",
170
+ "client_family": "web",
171
+ "provider_names": ["Google"],
172
+ "explicit_auth_flows": ["ALLOW_REFRESH_TOKEN_AUTH"],
173
+ "callback_urls": ["https://app.example/auth/callback"],
174
+ "logout_urls": ["https://app.example"],
175
+ "lifecycle_state": "native",
176
+ },
177
+ "web:microsoft": {
178
+ "client_id": "zyxwvutsrqponmlkjihgfedcba",
179
+ "route_key": "microsoft",
180
+ "client_family": "web",
181
+ "provider_names": ["MicrosoftOrganizations"],
182
+ "explicit_auth_flows": ["ALLOW_REFRESH_TOKEN_AUTH"],
183
+ "callback_urls": ["https://app.example/auth/callback"],
184
+ "logout_urls": ["https://app.example"],
185
+ "lifecycle_state": "native",
186
+ },
187
+ }
188
+ },
189
+ }
190
+ scope = {
191
+ "stage": "dev",
192
+ "account_id": "123456789012",
193
+ "region": "us-east-1",
194
+ "microsoft_oauth_tenant": "00000000-0000-4000-8000-000000000123",
195
+ }
196
+
197
+ payload, state = runner.native_auth_reconciliation_payload(outputs, scope, {})
198
+
199
+ assert payload["revision"] == 1
200
+ assert [item["connectionKey"] for item in payload["connections"]] == [
201
+ "google",
202
+ "local",
203
+ "microsoft:organizations",
204
+ ]
205
+ by_route = {item["routeKey"]: item for item in payload["routeClients"]}
206
+ assert by_route["local"]["providerNames"] == ["COGNITO"]
207
+ assert by_route["google"]["providerNames"] == ["Google"]
208
+ assert by_route["microsoft"]["providerNames"] == ["MicrosoftOrganizations"]
209
+ microsoft_connection = next(
210
+ item
211
+ for item in payload["connections"]
212
+ if item["connectionKey"] == "microsoft:organizations"
213
+ )
214
+ assert microsoft_connection["issuerUrl"] == (
215
+ "https://login.microsoftonline.com/00000000-0000-4000-8000-000000000123/v2.0"
216
+ )
217
+ assert "secret" not in json.dumps(payload).lower()
218
+ assert len(payload["manifestFingerprint"]) == 64
219
+
220
+ retry_payload, _ = runner.native_auth_reconciliation_payload(outputs, scope, {})
221
+ assert retry_payload["idempotencyKey"] == payload["idempotencyKey"]
222
+ assert retry_payload["manifestFingerprint"] == payload["manifestFingerprint"]
223
+
224
+ replay, replay_state = runner.native_auth_reconciliation_payload(outputs, scope, state)
225
+ assert replay is None
226
+ assert replay_state == state
227
+
228
+
229
+ def tenant_entra_operation(action: str, revision: int = 1) -> dict:
230
+ tenant_id = "00000000-0000-4000-8000-000000000123"
231
+ payload = {
232
+ "action": "update",
233
+ "operation": {
234
+ "kind": "identity_provider",
235
+ "action": action,
236
+ "revision": revision,
237
+ "expectedPreviousRevision": revision - 1,
238
+ "connection": {
239
+ "connectionKey": f"microsoft:tenant:{tenant_id}",
240
+ "tenantId": tenant_id,
241
+ "providerName": "Entra_0000000000004000_deadbeef",
242
+ "displayName": "Acme Microsoft",
243
+ "clientId": "entra-client-id",
244
+ "clientSecretRef": (
245
+ "arn:aws:secretsmanager:us-east-1:123456789012:"
246
+ f"secret:thinkwork/dev/auth/entra/{tenant_id}-ABC123"
247
+ ),
248
+ "issuerUrl": f"https://login.microsoftonline.com/{tenant_id}/v2.0",
249
+ "tenantBindings": [
250
+ {
251
+ "tenantId": "019f762b-683c-7153-acff-24ee932d73f6",
252
+ "label": "Acme",
253
+ "hostnames": ["Login.Acme.Example", "login.acme.example"],
254
+ }
255
+ ],
256
+ },
257
+ },
258
+ }
259
+ if action == "rotate":
260
+ payload["operation"]["connection"]["clientSecretVersionStage"] = "AWSPENDING"
261
+ return payload
262
+
263
+
264
+ def test_identity_provider_operation_builds_revisioned_secret_free_desired_state() -> None:
265
+ runner = load_runner()
266
+ payload = tenant_entra_operation("create")
267
+
268
+ desired = runner.identity_provider_desired_connections(
269
+ payload,
270
+ stage="dev",
271
+ account_id="123456789012",
272
+ region="us-east-1",
273
+ previous_state={},
274
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
275
+ )
276
+
277
+ assert len(desired) == 1
278
+ assert desired[0]["lifecycleState"] == "native"
279
+ assert desired[0]["tenantDirectoryId"] == "00000000-0000-4000-8000-000000000123"
280
+ assert desired[0]["tenantBindings"][0]["hostnames"] == ["login.acme.example"]
281
+ assert runner.tenant_entra_terraform_projection(desired) == [
282
+ {
283
+ "connection_key": "microsoft:tenant:00000000-0000-4000-8000-000000000123",
284
+ "tenant_id": "00000000-0000-4000-8000-000000000123",
285
+ "provider_name": "Entra_0000000000004000_deadbeef",
286
+ "display_name": "Acme Microsoft",
287
+ }
288
+ ]
289
+ assert "tenant-super-secret" not in json.dumps(desired)
290
+
291
+
292
+ def test_identity_provider_disable_removes_route_before_provider_retirement() -> None:
293
+ runner = load_runner()
294
+ created = runner.identity_provider_desired_connections(
295
+ tenant_entra_operation("create"),
296
+ stage="dev",
297
+ account_id="123456789012",
298
+ region="us-east-1",
299
+ previous_state={},
300
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
301
+ )
302
+ disabled = runner.identity_provider_desired_connections(
303
+ tenant_entra_operation("disable", revision=2),
304
+ stage="dev",
305
+ account_id="123456789012",
306
+ region="us-east-1",
307
+ previous_state={"desiredRevision": 1, "tenantConnections": created},
308
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
309
+ )
310
+
311
+ assert disabled[0]["lifecycleState"] == "denied"
312
+ assert disabled[0]["tenantBindings"][0]["status"] == "disabled"
313
+ assert runner.tenant_entra_terraform_projection(disabled) == []
314
+
315
+
316
+ def test_identity_provider_operation_rejects_cross_stage_secret_reference() -> None:
317
+ runner = load_runner()
318
+ payload = tenant_entra_operation("create")
319
+ payload["operation"]["connection"]["clientSecretRef"] = (
320
+ "arn:aws:secretsmanager:us-east-1:123456789012:secret:thinkwork/prod/auth/entra/example"
321
+ )
322
+
323
+ with pytest.raises(RuntimeError, match="account, region, stage"):
324
+ runner.identity_provider_desired_connections(
325
+ payload,
326
+ stage="dev",
327
+ account_id="123456789012",
328
+ region="us-east-1",
329
+ previous_state={},
330
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
331
+ )
332
+
333
+
334
+ def test_identity_provider_operation_rejects_invalid_secret_version_stage() -> None:
335
+ runner = load_runner()
336
+ payload = tenant_entra_operation("rotate", revision=2)
337
+ payload["operation"]["connection"]["clientSecretVersionStage"] = "AWS_PENDING"
338
+
339
+ with pytest.raises(RuntimeError, match="must be AWSPENDING"):
340
+ runner.identity_provider_desired_connections(
341
+ payload,
342
+ stage="dev",
343
+ account_id="123456789012",
344
+ region="us-east-1",
345
+ previous_state={"desiredRevision": 1, "tenantConnections": []},
346
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
347
+ )
348
+
349
+
350
+ def test_identity_provider_operation_rejects_pending_stage_outside_rotation() -> None:
351
+ runner = load_runner()
352
+ payload = tenant_entra_operation("create")
353
+ payload["operation"]["connection"]["clientSecretVersionStage"] = "AWSPENDING"
354
+
355
+ with pytest.raises(RuntimeError, match="valid only.*rotation"):
356
+ runner.identity_provider_desired_connections(
357
+ payload,
358
+ stage="dev",
359
+ account_id="123456789012",
360
+ region="us-east-1",
361
+ previous_state={},
362
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
363
+ )
364
+
365
+
366
+ def test_identity_provider_rotation_requires_pending_stage() -> None:
367
+ runner = load_runner()
368
+ created = runner.identity_provider_desired_connections(
369
+ tenant_entra_operation("create"),
370
+ stage="dev",
371
+ account_id="123456789012",
372
+ region="us-east-1",
373
+ previous_state={},
374
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
375
+ )
376
+ payload = tenant_entra_operation("rotate", revision=2)
377
+ payload["operation"]["connection"].pop("clientSecretVersionStage")
378
+
379
+ with pytest.raises(RuntimeError, match="requires clientSecretVersionStage AWSPENDING"):
380
+ runner.identity_provider_desired_connections(
381
+ payload,
382
+ stage="dev",
383
+ account_id="123456789012",
384
+ region="us-east-1",
385
+ previous_state={"desiredRevision": 1, "tenantConnections": created},
386
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
387
+ )
388
+
389
+
390
+ def test_identity_provider_update_does_not_require_agent_image_pin() -> None:
391
+ runner = load_runner()
392
+
393
+ assert (
394
+ runner.resolve_agentcore_pi_source_image_uri(
395
+ {"action": "update", "operation": {"kind": "identity_provider"}}
396
+ )
397
+ == ""
398
+ )
399
+
400
+
401
+ def test_identity_provider_rotate_reads_pending_secret_and_promotes_only_after_reconcile(
402
+ monkeypatch: pytest.MonkeyPatch,
403
+ ) -> None:
404
+ runner = load_runner()
405
+ connection = runner.identity_provider_desired_connections(
406
+ tenant_entra_operation("rotate", revision=2),
407
+ stage="dev",
408
+ account_id="123456789012",
409
+ region="us-east-1",
410
+ previous_state={
411
+ "desiredRevision": 1,
412
+ "tenantConnections": runner.identity_provider_desired_connections(
413
+ tenant_entra_operation("create"),
414
+ stage="dev",
415
+ account_id="123456789012",
416
+ region="us-east-1",
417
+ previous_state={},
418
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
419
+ ),
420
+ },
421
+ current_outputs={"user_pool_id": {"value": "us-east-1_Example123"}},
422
+ )[0]
423
+ calls = []
424
+
425
+ def fake_output(args, **_kwargs):
426
+ calls.append(args)
427
+ if "get-secret-value" in args:
428
+ return json.dumps({"clientId": "entra-client-id", "clientSecret": "pending-secret"})
429
+ if "describe-secret" in args:
430
+ return json.dumps(
431
+ {"pending-version": ["AWSPENDING"], "current-version": ["AWSCURRENT"]}
432
+ )
433
+ return "{}"
434
+
435
+ monkeypatch.setattr(runner, "output", fake_output)
436
+ assert runner._read_tenant_entra_secret(connection) == "pending-secret"
437
+ assert "AWSPENDING" in calls[0]
438
+
439
+ vars_json = {"auth_tenant_connection_metadata": [connection]}
440
+ promoted = runner.promote_tenant_entra_pending_secret(
441
+ tenant_entra_operation("rotate", revision=2), vars_json
442
+ )
443
+ assert promoted["status"] == "promoted"
444
+ promotion = next(args for args in calls if "update-secret-version-stage" in args)
445
+ assert "pending-version" in promotion
446
+ assert "current-version" in promotion
447
+
448
+
449
+ def test_bootstrap_can_fall_back_to_release_agentcore_pi_image(
450
+ monkeypatch: pytest.MonkeyPatch,
451
+ ) -> None:
452
+ runner = load_runner()
453
+ monkeypatch.setattr(
454
+ runner,
455
+ "release_runtime_image",
456
+ lambda name: (
457
+ "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
458
+ if name == "agentcore-pi-amd64"
459
+ else ""
460
+ ),
461
+ )
462
+
463
+ assert runner.resolve_agentcore_pi_source_image_uri({"action": "deploy"}) == (
464
+ "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
465
+ )
466
+
467
+
97
468
  def test_sync_release_artifacts_stages_artifacts_from_platform_bundle(
98
469
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
99
470
  ) -> None:
@@ -562,6 +933,61 @@ def run_push_database_schema(
562
933
  return db.events
563
934
 
564
935
 
936
+ def test_prepare_additive_schema_before_app_applies_data_before_app_retirement(
937
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
938
+ ) -> None:
939
+ runner = load_runner()
940
+ commands: list[list[str]] = []
941
+ pushed_vars: list[dict] = []
942
+
943
+ def fake_run(command, **_kwargs):
944
+ commands.append(command)
945
+ return SimpleNamespace(returncode=0)
946
+
947
+ monkeypatch.setattr(runner, "TF", tmp_path)
948
+ monkeypatch.setattr(runner, "TERRAFORM_EVIDENCE", {})
949
+ monkeypatch.setattr(runner.subprocess, "run", fake_run)
950
+ monkeypatch.setattr(runner, "output", lambda *_args, **_kwargs: "{}")
951
+ monkeypatch.setattr(
952
+ runner,
953
+ "push_database_schema",
954
+ lambda _outputs_path, vars_json: pushed_vars.append(vars_json),
955
+ )
956
+
957
+ result = runner.prepare_additive_schema_before_app(
958
+ {"action": "deploy"},
959
+ {
960
+ "stage": "prod",
961
+ "auth_retirement_phase": "retired",
962
+ "finalize_auth_retirement": True,
963
+ },
964
+ )
965
+
966
+ assert result.returncode == 0
967
+ assert commands == [
968
+ [
969
+ "terraform",
970
+ "plan",
971
+ "-target=module.thinkwork.module.database",
972
+ "-out=tfplan-data",
973
+ "-no-color",
974
+ ],
975
+ ["terraform", "apply", "-auto-approve", "-no-color", "tfplan-data"],
976
+ ]
977
+ assert pushed_vars == [
978
+ {
979
+ "stage": "prod",
980
+ "auth_retirement_phase": "retired",
981
+ "finalize_auth_retirement": False,
982
+ }
983
+ ]
984
+ assert runner.TERRAFORM_EVIDENCE["schemaOrdering"] == {
985
+ "status": "succeeded",
986
+ "target": "module.thinkwork.module.database",
987
+ "phase": "foundation-data-additive-before-app",
988
+ }
989
+
990
+
565
991
  def test_push_database_schema_checks_out_release_selected_module_source(
566
992
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
567
993
  ) -> None:
@@ -648,6 +1074,47 @@ def test_push_database_schema_applies_only_unrecorded_migrations(
648
1074
  )
649
1075
 
650
1076
 
1077
+ @pytest.mark.parametrize(
1078
+ ("phase", "finalize", "expected"),
1079
+ [
1080
+ ("coexistence", False, []),
1081
+ ("coexistence", True, []),
1082
+ ("cutover", False, []),
1083
+ ("cutover", True, []),
1084
+ ("retired", False, []),
1085
+ ("retired", True, ["0263_drop_workos_auth_runtime.sql"]),
1086
+ ],
1087
+ )
1088
+ def test_push_database_schema_requires_retired_phase_and_explicit_finalization(
1089
+ tmp_path: Path,
1090
+ monkeypatch: pytest.MonkeyPatch,
1091
+ phase: str,
1092
+ finalize: bool,
1093
+ expected: list[str],
1094
+ ) -> None:
1095
+ runner = load_runner()
1096
+ monkeypatch.setattr(runner, "POST_SEED_MIGRATIONS", [])
1097
+ db = FakeLedgerDb(tenants_exist=True, ledger=set())
1098
+ run_push_database_schema(
1099
+ runner,
1100
+ tmp_path,
1101
+ monkeypatch,
1102
+ db,
1103
+ ["0263_drop_workos_auth_runtime.sql"],
1104
+ migration_bodies={
1105
+ "0263_drop_workos_auth_runtime.sql": ("-- deployment-phase: auth-retired\nSELECT 1;\n")
1106
+ },
1107
+ vars_json={
1108
+ "stage": "tei-e2e",
1109
+ "auth_retirement_phase": phase,
1110
+ "finalize_auth_retirement": finalize,
1111
+ },
1112
+ )
1113
+
1114
+ assert db.applied_files == expected
1115
+ assert ("0263_drop_workos_auth_runtime.sql" in db.ledger) is bool(expected)
1116
+
1117
+
651
1118
  def test_push_database_schema_transition_backfills_ledger_via_markers(
652
1119
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
653
1120
  ) -> None:
@@ -685,6 +1152,40 @@ def test_push_database_schema_transition_backfills_ledger_via_markers(
685
1152
  assert db.ledger["0001_ancient.sql"] == "transition-assumed"
686
1153
 
687
1154
 
1155
+ def test_push_database_schema_transition_applies_post_cutoff_migrations(
1156
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1157
+ ) -> None:
1158
+ runner = load_runner()
1159
+ db = FakeLedgerDb(
1160
+ tenants_exist=True,
1161
+ ledger=None,
1162
+ present_objects={"public.existing_native_auth_table"},
1163
+ )
1164
+ run_push_database_schema(
1165
+ runner,
1166
+ tmp_path,
1167
+ monkeypatch,
1168
+ db,
1169
+ [
1170
+ "0155_tenant_model_catalog.sql",
1171
+ "0261_native_auth_control_plane.sql",
1172
+ "0262_auth_subscription_tickets.sql",
1173
+ ],
1174
+ migration_bodies={
1175
+ "0261_native_auth_control_plane.sql": (
1176
+ "-- creates: public.missing_native_auth_table\nSELECT 1;\n"
1177
+ ),
1178
+ "0262_auth_subscription_tickets.sql": (
1179
+ "-- creates: public.existing_native_auth_table\nSELECT 1;\n"
1180
+ ),
1181
+ },
1182
+ )
1183
+
1184
+ assert "0261_native_auth_control_plane.sql" in db.applied_files
1185
+ assert db.ledger["0261_native_auth_control_plane.sql"] == "runner"
1186
+ assert db.ledger["0262_auth_subscription_tickets.sql"] == "transition-verified"
1187
+
1188
+
688
1189
  def test_push_database_schema_greenfield_records_full_ledger(
689
1190
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
690
1191
  ) -> None:
@@ -995,6 +1496,7 @@ def _cognito_email_runner_env(runner, tmp_path: Path, monkeypatch: pytest.Monkey
995
1496
  monkeypatch.setenv("THINKWORK_TERRAFORM_LOCK_TABLE", "thinkwork-locks")
996
1497
  monkeypatch.setenv("THINKWORK_RELEASE_ARTIFACT_BUCKET", "thinkwork-artifacts")
997
1498
  monkeypatch.setenv("THINKWORK_RELEASE_VERSION", "v0.1.0-canary.150")
1499
+ monkeypatch.setenv("AUTH_MIGRATION_RECOVERY_DEADLINE", "2099-01-01T00:00:00Z")
998
1500
  monkeypatch.setenv("THINKWORK_RELEASE_MANIFEST_URL", file_url(manifest_path))
999
1501
  monkeypatch.setenv("THINKWORK_RELEASE_MANIFEST_SHA256", manifest_sha)
1000
1502
  return tf_dir
@@ -1101,9 +1603,17 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1101
1603
  "apiAuthSecret": "api-secret",
1102
1604
  "cognitoEmailSourceArn": "arn:aws:ses:us-east-1:637423202447:identity/payload.example.com",
1103
1605
  },
1104
- {"cognitoEmailSourceArn": secret_arn},
1606
+ {
1607
+ "cognitoEmailSourceArn": secret_arn,
1608
+ "authRetirementPhase": "cutover",
1609
+ },
1105
1610
  )
1106
1611
  assert vars_json["cognito_email_source_arn"] == secret_arn
1612
+ assert vars_json["auth_retirement_phase"] == "cutover"
1613
+ assert vars_json["finalize_auth_retirement"] is False
1614
+ assert "auth_retirement_phase = var.auth_retirement_phase" in (runner.TF / "main.tf").read_text(
1615
+ encoding="utf-8"
1616
+ )
1107
1617
 
1108
1618
  vars_json_default = runner.write_runner_files(
1109
1619
  {
@@ -1120,6 +1630,150 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1120
1630
  assert vars_json_default["cognito_reply_to_email_address"] == ""
1121
1631
  assert vars_json_default["app_domain"] == ""
1122
1632
  assert vars_json_default["app_certificate_arn"] == ""
1633
+ assert vars_json_default["auth_retirement_phase"] == "retired"
1634
+ assert vars_json_default["finalize_auth_retirement"] is False
1635
+
1636
+ vars_json_retired = runner.write_runner_files(
1637
+ {
1638
+ "stage": "tei-e2e",
1639
+ "awsRegion": "us-east-1",
1640
+ "awsAccountId": "637423202447",
1641
+ "dbPassword": "db-secret",
1642
+ "apiAuthSecret": "api-secret",
1643
+ "authRetirementPhase": "retired",
1644
+ "finalizeAuthRetirement": True,
1645
+ },
1646
+ {},
1647
+ )
1648
+ assert vars_json_retired["auth_retirement_phase"] == "retired"
1649
+ assert vars_json_retired["finalize_auth_retirement"] is True
1650
+
1651
+
1652
+ def test_write_runner_files_defaults_existing_preauth_stack_to_coexistence(
1653
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1654
+ ) -> None:
1655
+ runner = load_runner()
1656
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1657
+ monkeypatch.setattr(
1658
+ runner,
1659
+ "current_terraform_state",
1660
+ lambda _stage: {
1661
+ "outputs": {"user_pool_id": {"value": "us-east-1_Existing"}},
1662
+ "resources": [{"type": "aws_cognito_user_pool", "name": "main"}],
1663
+ },
1664
+ )
1665
+
1666
+ vars_json = runner.write_runner_files(
1667
+ {
1668
+ "stage": "tei-e2e",
1669
+ "awsRegion": "us-east-1",
1670
+ "awsAccountId": "637423202447",
1671
+ "dbPassword": "db-secret",
1672
+ "apiAuthSecret": "api-secret",
1673
+ },
1674
+ {},
1675
+ )
1676
+
1677
+ assert vars_json["auth_retirement_phase"] == "coexistence"
1678
+ assert vars_json["auth_migration_recovery_deadline"] == "2099-01-01T00:00:00Z"
1679
+
1680
+
1681
+ def test_write_runner_files_requires_deadline_for_coexistence(
1682
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1683
+ ) -> None:
1684
+ runner = load_runner()
1685
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1686
+ monkeypatch.delenv("AUTH_MIGRATION_RECOVERY_DEADLINE")
1687
+
1688
+ with pytest.raises(RuntimeError, match="authMigrationRecoveryDeadline is required"):
1689
+ runner.write_runner_files(
1690
+ {
1691
+ "stage": "tei-e2e",
1692
+ "awsRegion": "us-east-1",
1693
+ "awsAccountId": "637423202447",
1694
+ "dbPassword": "db-secret",
1695
+ "apiAuthSecret": "api-secret",
1696
+ "authRetirementPhase": "coexistence",
1697
+ },
1698
+ {},
1699
+ )
1700
+
1701
+
1702
+ def test_write_runner_files_preserves_deployed_auth_retirement_phase(
1703
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1704
+ ) -> None:
1705
+ runner = load_runner()
1706
+ _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1707
+ monkeypatch.setattr(
1708
+ runner,
1709
+ "current_terraform_state",
1710
+ lambda _stage: {
1711
+ "outputs": {
1712
+ "auth_retirement_phase": {"value": "cutover"},
1713
+ "user_pool_id": {"value": "us-east-1_Existing"},
1714
+ }
1715
+ },
1716
+ )
1717
+
1718
+ vars_json = runner.write_runner_files(
1719
+ {
1720
+ "stage": "tei-e2e",
1721
+ "awsRegion": "us-east-1",
1722
+ "awsAccountId": "637423202447",
1723
+ "dbPassword": "db-secret",
1724
+ "apiAuthSecret": "api-secret",
1725
+ },
1726
+ {},
1727
+ )
1728
+
1729
+ assert vars_json["auth_retirement_phase"] == "cutover"
1730
+
1731
+
1732
+ def test_main_reconciles_native_auth_before_destructive_schema() -> None:
1733
+ source = Path(__file__).with_name("runner.py").read_text(encoding="utf-8")
1734
+ post_apply = source[source.index("def main():") :]
1735
+ reconciliation = post_apply.index(
1736
+ "CONTROLLER_EVIDENCE[\"authReconciliation\"] = reconcile_native_auth_metadata"
1737
+ )
1738
+ schema = post_apply.index("push_database_schema(outputs_path, vars_json)", reconciliation)
1739
+
1740
+ assert reconciliation < schema
1741
+
1742
+
1743
+ def test_write_runner_files_wires_analyst_lambda_vpc_egress(
1744
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1745
+ ) -> None:
1746
+ """analyst_lambda_vpc_egress needs all three wiring points (vars_json,
1747
+ root variable, module argument) and reads runner-secrets first — the
1748
+ durable per-environment home for the flag — with a payload fallback.
1749
+ Secrets Manager JSON strings ("true") must land as real booleans."""
1750
+ runner = load_runner()
1751
+ tf_dir = _cognito_email_runner_env(runner, tmp_path, monkeypatch)
1752
+ base_payload = {
1753
+ "stage": "tei-e2e",
1754
+ "awsRegion": "us-east-1",
1755
+ "awsAccountId": "637423202447",
1756
+ "dbPassword": "db-secret",
1757
+ "apiAuthSecret": "api-secret",
1758
+ }
1759
+
1760
+ assert runner.write_runner_files(base_payload, {})["analyst_lambda_vpc_egress"] is False
1761
+ assert (
1762
+ runner.write_runner_files(base_payload, {"analystLambdaVpcEgress": "true"})[
1763
+ "analyst_lambda_vpc_egress"
1764
+ ]
1765
+ is True
1766
+ )
1767
+ assert (
1768
+ runner.write_runner_files({**base_payload, "analystLambdaVpcEgress": True}, {})[
1769
+ "analyst_lambda_vpc_egress"
1770
+ ]
1771
+ is True
1772
+ )
1773
+
1774
+ main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
1775
+ assert 'variable "analyst_lambda_vpc_egress"' in main_tf
1776
+ assert "= var.analyst_lambda_vpc_egress" in main_tf
1123
1777
 
1124
1778
 
1125
1779
  def test_write_runner_files_threads_customer_domain_vars_from_payload(
@@ -2720,7 +3374,6 @@ def test_runtime_profile_contains_customer_authority_metadata(
2720
3374
  "app_url": {"value": "https://app.example.com"},
2721
3375
  "appsync_api_url": {"value": "https://appsync.example.com/graphql"},
2722
3376
  "appsync_realtime_url": {"value": "wss://appsync.example.com/graphql"},
2723
- "appsync_api_key": {"value": "api-key"},
2724
3377
  "auth_domain": {"value": "thinkwork-tei-e2e"},
2725
3378
  "user_pool_id": {"value": "us-east-1_abc"},
2726
3379
  "admin_client_id": {"value": "client-id"},