thinkwork-cli 0.13.1 → 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 (130) hide show
  1. package/dist/{api-client-4DZ266J5.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-Y2WHJECR.js → chunk-Z2F6RJFR.js} +1 -39
  15. package/dist/cli.js +43080 -13150
  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 +6 -0
  38. package/dist/drizzle/0230_analyst_rls.sql +9 -0
  39. package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
  40. package/dist/drizzle/0251_brain_stragglers.sql +21 -0
  41. package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
  42. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  43. package/dist/drizzle/0261_managed_harness_multiplayer.sql +313 -0
  44. package/dist/drizzle/0261_native_auth_control_plane.sql +328 -0
  45. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  46. package/dist/drizzle/0262_parallel_harness_threads.sql +9 -0
  47. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  48. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  49. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  50. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +113 -0
  51. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  52. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  53. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  54. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  55. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  56. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  57. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  58. package/dist/event-streams-7JNLJEEL.js +894 -0
  59. package/dist/event-streams-KDF5RWR6.js +1515 -0
  60. package/dist/loadSso-DJVB7BXE.js +576 -0
  61. package/dist/loadSso-JAMNA3WY.js +583 -0
  62. package/dist/plugins/catalog/package.json +1 -2
  63. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +0 -1
  64. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +0 -69
  65. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +0 -2
  66. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -5
  67. package/dist/plugins/catalog/src/contracts.ts +2 -179
  68. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -6
  69. package/dist/signin-UGDWTGS4.js +821 -0
  70. package/dist/sso-oidc-Q4VXANXY.js +837 -0
  71. package/dist/sts-CWLSE3GH.js +1241 -0
  72. package/dist/sts-DNZLNVOV.js +1245 -0
  73. package/dist/terraform/examples/greenfield/main.tf +223 -66
  74. package/dist/terraform/examples/greenfield/terraform.tfvars.example +15 -1
  75. package/dist/terraform/modules/app/agentcore-gateway/main.tf +305 -0
  76. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +44 -0
  77. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +93 -0
  78. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +33 -0
  79. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +543 -0
  80. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +70 -0
  81. package/dist/terraform/modules/app/agentcore-harness/main.tf +237 -8
  82. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +63 -0
  83. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +5 -0
  84. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +430 -0
  85. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +180 -0
  86. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +193 -0
  87. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +5 -0
  88. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +5 -0
  89. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +5 -0
  90. package/dist/terraform/modules/app/agentcore-harness/variables.tf +72 -1
  91. package/dist/terraform/modules/app/agentcore-identity/main.tf +173 -0
  92. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  93. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +122 -0
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +25 -0
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +40 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +74 -0
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +61 -0
  100. package/dist/terraform/modules/app/agentcore-identity/variables.tf +54 -0
  101. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  102. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  103. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  104. package/dist/terraform/modules/app/deployment-control-plane/runner.py +966 -27
  105. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +581 -22
  106. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +88 -0
  107. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +91 -0
  108. package/dist/terraform/modules/app/lambda-api/handlers.tf +298 -74
  109. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +99 -54
  110. package/dist/terraform/modules/app/lambda-api/main.tf +24 -0
  111. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +181 -4
  112. package/dist/terraform/modules/app/lambda-api/outputs.tf +35 -0
  113. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  114. package/dist/terraform/modules/app/lambda-api/variables.tf +109 -16
  115. package/dist/terraform/modules/foundation/cognito/main.tf +294 -72
  116. package/dist/terraform/modules/foundation/cognito/outputs.tf +42 -0
  117. package/dist/terraform/modules/foundation/cognito/variables.tf +114 -4
  118. package/dist/terraform/modules/thinkwork/main.tf +269 -46
  119. package/dist/terraform/modules/thinkwork/outputs.tf +136 -8
  120. package/dist/terraform/modules/thinkwork/variables.tf +124 -17
  121. package/dist/terraform/schema.graphql +69 -36
  122. package/dist/workspace-defaults/files/USER.md +1 -0
  123. package/package.json +3 -2
  124. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
  125. package/dist/plugins/workos-auth/package.json +0 -22
  126. package/dist/plugins/workos-auth/src/index.ts +0 -30
  127. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  128. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  129. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  130. package/dist/plugins/workos-auth/tsconfig.json +0 -8
@@ -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
 
@@ -125,18 +126,324 @@ def test_customer_update_rejects_non_customer_agentcore_pi_source_image() -> Non
125
126
  def test_customer_update_accepts_customer_ecr_agentcore_pi_source_image() -> None:
126
127
  runner = load_runner()
127
128
  image = (
128
- "637423202447.dkr.ecr.us-east-1.amazonaws.com/"
129
- "thinkwork-tei-e2e-agentcore:pinned@sha256:abc"
129
+ "637423202447.dkr.ecr.us-east-1.amazonaws.com/thinkwork-tei-e2e-agentcore:pinned@sha256:abc"
130
130
  )
131
131
 
132
- assert runner.resolve_agentcore_pi_source_image_uri(
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) == [
133
282
  {
134
- "action": "update",
135
- "awsAccountId": "637423202447",
136
- "awsRegion": "us-east-1",
137
- "agentcorePiSourceImageUri": image,
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",
138
287
  }
139
- ) == image
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
140
447
 
141
448
 
142
449
  def test_bootstrap_can_fall_back_to_release_agentcore_pi_image(
@@ -146,9 +453,11 @@ def test_bootstrap_can_fall_back_to_release_agentcore_pi_image(
146
453
  monkeypatch.setattr(
147
454
  runner,
148
455
  "release_runtime_image",
149
- lambda name: "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
150
- if name == "agentcore-pi-amd64"
151
- else "",
456
+ lambda name: (
457
+ "ghcr.io/thinkwork-ai/thinkwork-agentcore@sha256:abc"
458
+ if name == "agentcore-pi-amd64"
459
+ else ""
460
+ ),
152
461
  )
153
462
 
154
463
  assert runner.resolve_agentcore_pi_source_image_uri({"action": "deploy"}) == (
@@ -624,6 +933,61 @@ def run_push_database_schema(
624
933
  return db.events
625
934
 
626
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
+
627
991
  def test_push_database_schema_checks_out_release_selected_module_source(
628
992
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
629
993
  ) -> None:
@@ -710,6 +1074,47 @@ def test_push_database_schema_applies_only_unrecorded_migrations(
710
1074
  )
711
1075
 
712
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
+
713
1118
  def test_push_database_schema_transition_backfills_ledger_via_markers(
714
1119
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
715
1120
  ) -> None:
@@ -747,6 +1152,40 @@ def test_push_database_schema_transition_backfills_ledger_via_markers(
747
1152
  assert db.ledger["0001_ancient.sql"] == "transition-assumed"
748
1153
 
749
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
+
750
1189
  def test_push_database_schema_greenfield_records_full_ledger(
751
1190
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
752
1191
  ) -> None:
@@ -1057,6 +1496,7 @@ def _cognito_email_runner_env(runner, tmp_path: Path, monkeypatch: pytest.Monkey
1057
1496
  monkeypatch.setenv("THINKWORK_TERRAFORM_LOCK_TABLE", "thinkwork-locks")
1058
1497
  monkeypatch.setenv("THINKWORK_RELEASE_ARTIFACT_BUCKET", "thinkwork-artifacts")
1059
1498
  monkeypatch.setenv("THINKWORK_RELEASE_VERSION", "v0.1.0-canary.150")
1499
+ monkeypatch.setenv("AUTH_MIGRATION_RECOVERY_DEADLINE", "2099-01-01T00:00:00Z")
1060
1500
  monkeypatch.setenv("THINKWORK_RELEASE_MANIFEST_URL", file_url(manifest_path))
1061
1501
  monkeypatch.setenv("THINKWORK_RELEASE_MANIFEST_SHA256", manifest_sha)
1062
1502
  return tf_dir
@@ -1163,9 +1603,17 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1163
1603
  "apiAuthSecret": "api-secret",
1164
1604
  "cognitoEmailSourceArn": "arn:aws:ses:us-east-1:637423202447:identity/payload.example.com",
1165
1605
  },
1166
- {"cognitoEmailSourceArn": secret_arn},
1606
+ {
1607
+ "cognitoEmailSourceArn": secret_arn,
1608
+ "authRetirementPhase": "cutover",
1609
+ },
1167
1610
  )
1168
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
+ )
1169
1617
 
1170
1618
  vars_json_default = runner.write_runner_files(
1171
1619
  {
@@ -1182,6 +1630,114 @@ def test_write_runner_files_cognito_email_vars_prefer_runner_secrets_and_default
1182
1630
  assert vars_json_default["cognito_reply_to_email_address"] == ""
1183
1631
  assert vars_json_default["app_domain"] == ""
1184
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
1185
1741
 
1186
1742
 
1187
1743
  def test_write_runner_files_wires_analyst_lambda_vpc_egress(
@@ -1201,15 +1757,19 @@ def test_write_runner_files_wires_analyst_lambda_vpc_egress(
1201
1757
  "apiAuthSecret": "api-secret",
1202
1758
  }
1203
1759
 
1204
- assert runner.write_runner_files(base_payload, {})[
1205
- "analyst_lambda_vpc_egress"
1206
- ] is False
1207
- assert runner.write_runner_files(
1208
- base_payload, {"analystLambdaVpcEgress": "true"}
1209
- )["analyst_lambda_vpc_egress"] is True
1210
- assert runner.write_runner_files(
1211
- {**base_payload, "analystLambdaVpcEgress": True}, {}
1212
- )["analyst_lambda_vpc_egress"] is True
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
+ )
1213
1773
 
1214
1774
  main_tf = (tf_dir / "main.tf").read_text(encoding="utf-8")
1215
1775
  assert 'variable "analyst_lambda_vpc_egress"' in main_tf
@@ -2814,7 +3374,6 @@ def test_runtime_profile_contains_customer_authority_metadata(
2814
3374
  "app_url": {"value": "https://app.example.com"},
2815
3375
  "appsync_api_url": {"value": "https://appsync.example.com/graphql"},
2816
3376
  "appsync_realtime_url": {"value": "wss://appsync.example.com/graphql"},
2817
- "appsync_api_key": {"value": "api-key"},
2818
3377
  "auth_domain": {"value": "thinkwork-tei-e2e"},
2819
3378
  "user_pool_id": {"value": "us-east-1_abc"},
2820
3379
  "admin_client_id": {"value": "client-id"},