thinkwork-cli 0.13.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
  3. package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
  4. package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
  5. package/dist/chunk-MWQ7RO76.js +137 -0
  6. package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
  7. package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
  8. package/dist/cli.js +4400 -16431
  9. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  11. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  12. package/dist/{dist-es-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
  13. package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
  14. package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
  15. package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
  16. package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
  17. package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
  18. package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
  19. package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
  20. package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
  21. package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
  22. package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
  23. package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
  24. package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
  25. package/dist/drizzle/0230_analyst_rls.sql +28 -45
  26. package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
  27. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  28. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  29. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  30. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  31. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  32. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  33. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  34. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  35. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  36. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  37. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  38. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  39. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  40. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  41. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  42. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  43. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  44. package/dist/drizzle/0287_brain_subject.sql +25 -0
  45. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  46. package/dist/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
  47. package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
  48. package/dist/plugins/README.md +2 -2
  49. package/dist/plugins/catalog/package.json +0 -3
  50. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
  51. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  52. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
  53. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
  54. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
  55. package/dist/plugins/catalog/src/contracts.ts +15 -0
  56. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
  57. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  58. package/dist/plugins/twenty/package.json +2 -1
  59. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  60. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  61. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  62. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  63. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  64. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  65. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  66. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  67. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  68. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  69. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  70. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  71. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  72. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  73. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  74. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  75. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  76. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  77. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  78. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  79. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  80. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  81. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  82. package/dist/scripts/post-deploy.sh +79 -1
  83. package/dist/{signin-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
  84. package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
  85. package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
  86. package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
  87. package/dist/terraform/examples/greenfield/main.tf +233 -175
  88. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -8
  89. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  90. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  91. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  92. package/dist/terraform/modules/app/agentcore-identity/main.tf +106 -26
  93. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +22 -7
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
  100. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
  101. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  102. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  103. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  104. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  105. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  106. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  107. package/dist/terraform/modules/app/agentcore-identity/variables.tf +6 -0
  108. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  109. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  110. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  111. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  112. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  113. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  114. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  115. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  116. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  117. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  118. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  119. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  120. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  121. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  122. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  123. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  124. package/dist/terraform/modules/app/deployment-control-plane/runner.py +2641 -124
  125. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
  126. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  127. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  128. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  129. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  130. package/dist/terraform/modules/app/lambda-api/handlers.tf +411 -723
  131. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
  132. package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
  133. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
  134. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  135. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  136. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -45
  137. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  138. package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
  139. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  140. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  141. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  142. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  143. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  144. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  145. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  146. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  147. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  148. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  149. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  150. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  151. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  152. package/dist/terraform/modules/foundation/cognito/main.tf +81 -8
  153. package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
  154. package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
  155. package/dist/terraform/modules/thinkwork/README.md +5 -1
  156. package/dist/terraform/modules/thinkwork/main.tf +376 -374
  157. package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
  158. package/dist/terraform/modules/thinkwork/variables.tf +246 -89
  159. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  160. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  161. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  162. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  163. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  164. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  165. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  166. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  167. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  168. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  169. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  170. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  171. package/package.json +1 -1
  172. package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
  173. package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
  174. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
  175. package/dist/plugins/company-data/README.md +0 -49
  176. package/dist/plugins/company-data/package.json +0 -21
  177. package/dist/plugins/company-data/src/index.ts +0 -30
  178. package/dist/plugins/company-data/src/manifest.ts +0 -26
  179. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  180. package/dist/plugins/company-data/tsconfig.json +0 -8
  181. package/dist/plugins/company-etl/README.md +0 -46
  182. package/dist/plugins/company-etl/package.json +0 -21
  183. package/dist/plugins/company-etl/src/index.ts +0 -29
  184. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  185. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  186. package/dist/plugins/company-etl/tsconfig.json +0 -8
  187. package/dist/plugins/lastmile/README.md +0 -50
  188. package/dist/plugins/lastmile/package.json +0 -23
  189. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  190. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  191. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  192. package/dist/plugins/lastmile/src/index.ts +0 -46
  193. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  194. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  195. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  196. package/dist/plugins/lastmile/tsconfig.json +0 -8
  197. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  198. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  199. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  200. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  201. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  202. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  203. package/dist/terraform/modules/app/agentcore-gateway/main.tf +0 -305
  204. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
  205. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
  206. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
  207. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
  208. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
  209. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
  210. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
  211. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
  212. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
  213. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
  214. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
  215. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
  216. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
  217. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
  218. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
  219. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  220. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  221. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +0 -88
  222. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
  223. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  224. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  225. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  226. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  227. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  228. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  229. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  230. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  231. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -4,119 +4,236 @@ set -euo pipefail
4
4
  : "${AWS_REGION:?AWS_REGION is required}"
5
5
  : "${TWENTY_OAUTH_ISSUER:?TWENTY_OAUTH_ISSUER is required}"
6
6
  : "${TWENTY_CLIENT_SECRET_ARN:?TWENTY_CLIENT_SECRET_ARN is required}"
7
- : "${TWENTY_ADMIN_TOKEN_SECRET_ARN:?TWENTY_ADMIN_TOKEN_SECRET_ARN is required}"
8
-
9
- callback_url="${TWENTY_CALLBACK_URL:-https://bedrock-agentcore.${AWS_REGION}.amazonaws.com/identities/oauth2/callback}"
10
-
11
- if existing_secret="$(aws secretsmanager get-secret-value \
12
- --region "$AWS_REGION" \
13
- --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
14
- --query SecretString \
15
- --output text 2>/dev/null)" &&
16
- jq -e '.client_id | type == "string" and length > 0' <<<"$existing_secret" >/dev/null &&
17
- jq -e '.client_secret | type == "string" and length > 0' <<<"$existing_secret" >/dev/null; then
18
- existing_callback="$(jq -r '.redirect_uris[0] // empty' <<<"$existing_secret")"
19
- if [[ "$existing_callback" == "$callback_url" ]]; then
20
- printf 'Twenty confidential OAuth client already bootstrapped: secret=%s\n' \
21
- "$TWENTY_CLIENT_SECRET_ARN"
22
- exit 0
7
+ : "${TWENTY_CREDENTIAL_PROVIDER_NAME:?TWENTY_CREDENTIAL_PROVIDER_NAME is required}"
8
+
9
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ issuer="${TWENTY_OAUTH_ISSUER%/}"
11
+
12
+ read_client_record() {
13
+ aws secretsmanager get-secret-value \
14
+ --region "$AWS_REGION" \
15
+ --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
16
+ --query SecretString \
17
+ --output text 2>/dev/null || printf '{}'
18
+ }
19
+
20
+ write_client_record() {
21
+ aws secretsmanager put-secret-value \
22
+ --region "$AWS_REGION" \
23
+ --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
24
+ --secret-string file:///dev/stdin >/dev/null
25
+ }
26
+
27
+ # Twenty's authorize endpoint is "$issuer/authorize" (NOT /oauth/authorize —
28
+ # observed live on tei-e2e: the previously hard-coded path 404s). Fetch the
29
+ # issuer's discovery document once and take both provider endpoints from it,
30
+ # falling back to the documented paths when discovery is unavailable.
31
+ discovery="$(curl --silent --fail --max-time 15 \
32
+ "$issuer/.well-known/oauth-authorization-server" 2>/dev/null || printf '')"
33
+ provider_endpoint() {
34
+ local field="$1" fallback="$2"
35
+ local discovered
36
+ discovered="$(jq -r --arg f "$field" '.[$f] // empty' <<<"$discovery" 2>/dev/null)"
37
+ if [[ -n "$discovered" ]]; then
38
+ printf '%s' "$discovered"
39
+ else
40
+ printf '%s' "$fallback"
23
41
  fi
24
- fi
42
+ }
25
43
 
26
- # Twenty's RFC 7591 endpoint intentionally downgrades all dynamic clients to
27
- # public clients. Its authenticated metadata API is the supported path for a
28
- # confidential application registration: Twenty generates and bcrypt-hashes
29
- # the secret server-side and returns the plaintext exactly once.
30
- admin_record="$(aws secretsmanager get-secret-value \
31
- --region "$AWS_REGION" \
32
- --secret-id "$TWENTY_ADMIN_TOKEN_SECRET_ARN" \
33
- --query SecretString \
34
- --output text)"
35
- admin_token="$(jq -r '.access_token // empty' <<<"$admin_record")"
36
- if [[ -z "$admin_token" ]]; then
37
- printf '%s\n' 'Twenty administrator token secret has no access_token' >&2
44
+ reconcile_provider() {
45
+ local client_id="$1"
46
+ local runtime_script="$script_dir/reconcile_twenty_provider.mjs"
47
+ if [[ -n "${THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR:-}" ]]; then
48
+ runtime_script="$THINKWORK_AGENTCORE_CONTROL_RUNTIME_DIR/reconcile_twenty_provider.js"
49
+ if [[ ! -f "$runtime_script" ]]; then
50
+ printf '%s\n' 'Managed AgentCore control runtime is missing reconcile_twenty_provider.js' >&2
51
+ return 66
52
+ fi
53
+ fi
54
+ jq -n \
55
+ --arg region "$AWS_REGION" \
56
+ --arg name "$TWENTY_CREDENTIAL_PROVIDER_NAME" \
57
+ --arg issuer "$issuer" \
58
+ --arg authorizationEndpoint "$(provider_endpoint authorization_endpoint "$issuer/authorize")" \
59
+ --arg tokenEndpoint "$(provider_endpoint token_endpoint "$issuer/oauth/token")" \
60
+ --arg clientId "$client_id" \
61
+ --arg secretArn "$TWENTY_CLIENT_SECRET_ARN" \
62
+ '{
63
+ region: $region,
64
+ name: $name,
65
+ issuer: $issuer,
66
+ authorizationEndpoint: $authorizationEndpoint,
67
+ tokenEndpoint: $tokenEndpoint,
68
+ clientId: $clientId,
69
+ secretArn: $secretArn
70
+ }' | node "$runtime_script"
71
+ }
72
+
73
+ verify_provider() {
74
+ local response="$1"
75
+ local expected_client_id="$2"
76
+ local expected_callback="$3"
77
+ jq -e \
78
+ --arg name "$TWENTY_CREDENTIAL_PROVIDER_NAME" \
79
+ --arg client_id "$expected_client_id" \
80
+ --arg callback "$expected_callback" \
81
+ --arg secret_arn "$TWENTY_CLIENT_SECRET_ARN" \
82
+ '.name == $name and
83
+ .clientId == $client_id and
84
+ .callbackUrl == $callback and
85
+ .clientSecretArn == $secret_arn and
86
+ .clientSecretJsonKey == "client_secret" and
87
+ .clientSecretSource == "EXTERNAL" and
88
+ .status == "READY"' <<<"$response" >/dev/null
89
+ }
90
+
91
+ client_record="$(read_client_record)"
92
+ if ! jq -e 'type == "object"' <<<"$client_record" >/dev/null 2>&1; then
93
+ printf '%s\n' 'Twenty client secret must contain a JSON object' >&2
38
94
  exit 1
39
95
  fi
40
96
 
41
- if [[ -n "${existing_secret:-}" ]] &&
42
- jq -e '.registration_id | type == "string" and length > 0' \
43
- <<<"$existing_secret" >/dev/null; then
44
- registration_id="$(jq -r '.registration_id' <<<"$existing_secret")"
45
- update_payload="$(jq -n \
46
- --arg id "$registration_id" \
47
- --arg callback "$callback_url" \
97
+ client_id="$(jq -r '.client_id // empty' <<<"$client_record")"
98
+ client_secret_ready=false
99
+ if [[ -n "$client_id" ]] &&
100
+ jq -e '.client_secret | type == "string" and length > 0' \
101
+ <<<"$client_record" >/dev/null 2>&1 &&
102
+ [[ "$(jq -r '.bootstrap_state // empty' <<<"$client_record")" == "ready" ]]; then
103
+ client_secret_ready=true
104
+ else
105
+ placeholder_id="thinkwork-bootstrap-$(openssl rand -hex 12)"
106
+ placeholder_secret="$(openssl rand -base64 48 | tr -d '\n')"
107
+ client_record="$(jq -n \
108
+ --arg client_id "$placeholder_id" \
109
+ --arg client_secret "$placeholder_secret" \
48
110
  '{
49
- query: "mutation UpdateAgentCoreRegistration($input: UpdateApplicationRegistrationInput!) { updateApplicationRegistration(input: $input) { id oAuthRedirectUris oAuthScopes } }",
50
- variables: {
51
- input: {
52
- id: $id,
53
- update: {oAuthRedirectUris: [$callback]}
54
- }
55
- }
111
+ bootstrap_state: "placeholder",
112
+ client_id: $client_id,
113
+ client_secret: $client_secret,
114
+ token_endpoint_auth_method: "client_secret_post"
56
115
  }')"
57
- update_response="$(curl --silent --show-error --fail-with-body \
58
- -H "authorization: Bearer $admin_token" \
59
- -H 'content-type: application/json' \
60
- --data-binary "$update_payload" \
61
- "${TWENTY_OAUTH_ISSUER%/}/metadata")"
62
- if jq -e '.errors | type == "array" and length > 0' <<<"$update_response" >/dev/null; then
63
- jq '{errors: [.errors[] | {message, extensions: {code: .extensions.code}}]}' \
64
- <<<"$update_response" >&2
65
- printf '%s\n' 'Twenty OAuth callback update failed' >&2
66
- exit 1
67
- fi
68
- updated_secret="$(jq --arg callback "$callback_url" \
69
- '.redirect_uris = [$callback]' <<<"$existing_secret")"
70
- printf '%s' "$updated_secret" | aws secretsmanager put-secret-value \
71
- --region "$AWS_REGION" \
72
- --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
73
- --secret-string file:///dev/stdin >/dev/null
74
- printf 'Twenty confidential OAuth client callback updated: secret=%s callback=%s\n' \
75
- "$TWENTY_CLIENT_SECRET_ARN" "$callback_url"
116
+ printf '%s' "$client_record" | write_client_record
117
+ client_id="$placeholder_id"
118
+ fi
119
+
120
+ # AgentCore issues a provider-specific callback only after the provider exists.
121
+ # The placeholder is never used for a user grant; it only lets AgentCore return
122
+ # the callback that Twenty must bind to the confidential client registration.
123
+ provider_response="$(reconcile_provider "$client_id")"
124
+ callback_url="$(jq -r '.callbackUrl // empty' <<<"$provider_response")"
125
+ if [[ -z "$callback_url" ]]; then
126
+ printf '%s\n' 'AgentCore Identity did not return a provider callback URL' >&2
127
+ exit 1
128
+ fi
129
+
130
+ registered_callback_matches=false
131
+ if $client_secret_ready &&
132
+ jq -e --arg callback "$callback_url" \
133
+ '.redirect_uris | type == "array" and index($callback) != null' \
134
+ <<<"$client_record" >/dev/null 2>&1 &&
135
+ [[ "$(jq -r '.token_endpoint_auth_method // empty' <<<"$client_record")" == \
136
+ "client_secret_post" ]]; then
137
+ registered_callback_matches=true
138
+ fi
139
+
140
+ if $registered_callback_matches; then
141
+ verify_provider "$provider_response" "$client_id" "$callback_url"
142
+ printf 'Twenty confidential OAuth client reused: provider=%s callback=%s\n' \
143
+ "$TWENTY_CREDENTIAL_PROVIDER_NAME" "$callback_url" >&2
144
+ printf '%s' "$provider_response"
76
145
  exit 0
77
146
  fi
78
147
 
79
- graphql_payload="$(jq -n \
148
+ if [[ -z "$discovery" ]]; then
149
+ printf '%s\n' 'Twenty discovery document is unavailable' >&2
150
+ exit 1
151
+ fi
152
+ registration_endpoint="$(jq -r '.registration_endpoint // empty' <<<"$discovery")"
153
+ if ! python3 - "$issuer" "$registration_endpoint" <<'PY'
154
+ import sys
155
+ from urllib.parse import urlsplit
156
+
157
+
158
+ def origin(url: str) -> tuple[str, str, int]:
159
+ parsed = urlsplit(url)
160
+ if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
161
+ raise ValueError("HTTPS origin without userinfo required")
162
+ return parsed.scheme, parsed.hostname.lower(), parsed.port or 443
163
+
164
+
165
+ try:
166
+ issuer_origin = origin(sys.argv[1])
167
+ registration_origin = origin(sys.argv[2])
168
+ except (ValueError, IndexError):
169
+ raise SystemExit(1)
170
+ raise SystemExit(0 if issuer_origin == registration_origin else 1)
171
+ PY
172
+ then
173
+ printf '%s\n' 'Twenty registration endpoint is not on the exact issuer origin' >&2
174
+ exit 1
175
+ fi
176
+ if ! jq -e \
177
+ --arg issuer "$issuer" \
178
+ '.issuer == $issuer and
179
+ (.grant_types_supported | index("authorization_code") != null) and
180
+ (.token_endpoint_auth_methods_supported | index("client_secret_post") != null) and
181
+ (.scopes_supported | index("api") != null) and
182
+ (.scopes_supported | index("profile") != null)' \
183
+ <<<"$discovery" >/dev/null; then
184
+ printf '%s\n' \
185
+ 'Twenty discovery does not advertise same-origin confidential authorization-code DCR' >&2
186
+ exit 1
187
+ fi
188
+
189
+ registration_payload="$(jq -n \
80
190
  --arg callback "$callback_url" \
81
191
  '{
82
- query: "mutation CreateAgentCoreRegistration($input: CreateApplicationRegistrationInput!) { createApplicationRegistration(input: $input) { applicationRegistration { id universalIdentifier oAuthClientId oAuthRedirectUris oAuthScopes } clientSecret } }",
83
- variables: {
84
- input: {
85
- name: "ThinkWork AgentCore Identity Twenty CRM",
86
- oAuthRedirectUris: [$callback],
87
- oAuthScopes: ["api", "profile"]
88
- }
89
- }
192
+ client_name: "ThinkWork AgentCore Identity Twenty CRM",
193
+ redirect_uris: [$callback],
194
+ grant_types: ["authorization_code"],
195
+ response_types: ["code"],
196
+ token_endpoint_auth_method: "client_secret_post",
197
+ scope: "api profile"
90
198
  }')"
91
- graphql_response="$(curl --silent --show-error --fail-with-body \
92
- -H "authorization: Bearer $admin_token" \
199
+ registration_response="$(curl --silent --show-error --fail-with-body \
93
200
  -H 'content-type: application/json' \
94
- --data-binary "$graphql_payload" \
95
- "${TWENTY_OAUTH_ISSUER%/}/metadata")"
201
+ --data-binary "$registration_payload" \
202
+ "$registration_endpoint")"
96
203
 
97
- if jq -e '.errors | type == "array" and length > 0' <<<"$graphql_response" >/dev/null; then
98
- jq '{errors: [.errors[] | {message, extensions: {code: .extensions.code}}]}' \
99
- <<<"$graphql_response" >&2
100
- printf '%s\n' 'Twenty confidential OAuth client bootstrap failed' >&2
204
+ client_record="$(jq -e \
205
+ --arg callback "$callback_url" \
206
+ '{
207
+ bootstrap_state: "ready",
208
+ client_id: .client_id,
209
+ client_secret: .client_secret,
210
+ client_id_issued_at: .client_id_issued_at,
211
+ client_secret_expires_at: .client_secret_expires_at,
212
+ client_name: .client_name,
213
+ redirect_uris: .redirect_uris,
214
+ grant_types: .grant_types,
215
+ response_types: .response_types,
216
+ token_endpoint_auth_method: .token_endpoint_auth_method,
217
+ scope: (.scope // "api profile"),
218
+ registration_access_token: .registration_access_token,
219
+ registration_client_uri: .registration_client_uri
220
+ } | select(
221
+ (.client_id | type == "string" and length > 0) and
222
+ (.client_secret | type == "string" and length > 0) and
223
+ (.redirect_uris | type == "array" and index($callback) != null) and
224
+ .token_endpoint_auth_method == "client_secret_post"
225
+ )' <<<"$registration_response")" || {
226
+ printf '%s\n' 'Twenty DCR did not return a confidential callback-bound client' >&2
101
227
  exit 1
102
- fi
228
+ }
229
+
230
+ # A Secrets Manager version write is atomic. The plaintext client secret never
231
+ # enters Terraform state or process arguments and is not printed to logs.
232
+ printf '%s' "$client_record" | write_client_record
233
+ client_id="$(jq -r '.client_id' <<<"$client_record")"
234
+ provider_response="$(reconcile_provider "$client_id")"
235
+ verify_provider "$provider_response" "$client_id" "$callback_url"
103
236
 
104
- client_record="$(jq -e '{
105
- client_id: .data.createApplicationRegistration.applicationRegistration.oAuthClientId,
106
- client_secret: .data.createApplicationRegistration.clientSecret,
107
- registration_id: .data.createApplicationRegistration.applicationRegistration.id,
108
- universal_identifier: .data.createApplicationRegistration.applicationRegistration.universalIdentifier,
109
- redirect_uris: .data.createApplicationRegistration.applicationRegistration.oAuthRedirectUris,
110
- scopes: .data.createApplicationRegistration.applicationRegistration.oAuthScopes
111
- } | select(
112
- (.client_id | type == "string" and length > 0) and
113
- (.client_secret | type == "string" and length > 0)
114
- )' <<<"$graphql_response")"
115
-
116
- printf '%s' "$client_record" | aws secretsmanager put-secret-value \
117
- --region "$AWS_REGION" \
118
- --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
119
- --secret-string file:///dev/stdin >/dev/null
120
-
121
- printf 'Twenty confidential OAuth client bootstrapped: secret=%s callback=%s\n' \
122
- "$TWENTY_CLIENT_SECRET_ARN" "$callback_url"
237
+ printf 'Twenty confidential OAuth client registered: provider=%s callback=%s\n' \
238
+ "$TWENTY_CREDENTIAL_PROVIDER_NAME" "$callback_url" >&2
239
+ printf '%s' "$provider_response"
@@ -13,13 +13,10 @@ if aws bedrock-agentcore-control get-oauth2-credential-provider \
13
13
  --name "$CREDENTIAL_PROVIDER_NAME" >/dev/null
14
14
  fi
15
15
 
16
- if aws bedrock-agentcore-control get-workload-identity \
17
- --region "$AWS_REGION" \
18
- --name "$WORKLOAD_IDENTITY_NAME" >/dev/null 2>&1; then
19
- aws bedrock-agentcore-control delete-workload-identity \
20
- --region "$AWS_REGION" \
21
- --name "$WORKLOAD_IDENTITY_NAME" >/dev/null
22
- fi
23
-
24
- printf 'AgentCore Identity proof resources deleted: workload=%s provider=%s\n' \
16
+ # THINK-324: the workload identity is SHARED with the live Twenty connector
17
+ # (per-user grants live under it). Its deletion is owned by
18
+ # workload_identity_cleanup_owner / delete_workload_identity.sh — retiring
19
+ # the proof plane must never delete it. See main.tf for the proof-only-stage
20
+ # leak trade-off.
21
+ printf 'AgentCore Identity proof provider deleted (workload identity retained): workload=%s provider=%s\n' \
25
22
  "$WORKLOAD_IDENTITY_NAME" "$CREDENTIAL_PROVIDER_NAME"
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # THINK-324 — final deletion of the SHARED workload identity, owned by the
5
+ # Twenty identity half (workload_identity_cleanup_owner). Runs only when the
6
+ # Twenty user-federation identity is itself being removed.
7
+
8
+ : "${AWS_REGION:?AWS_REGION is required}"
9
+ : "${WORKLOAD_IDENTITY_NAME:?WORKLOAD_IDENTITY_NAME is required}"
10
+
11
+ if aws bedrock-agentcore-control get-workload-identity \
12
+ --region "$AWS_REGION" \
13
+ --name "$WORKLOAD_IDENTITY_NAME" >/dev/null 2>&1; then
14
+ aws bedrock-agentcore-control delete-workload-identity \
15
+ --region "$AWS_REGION" \
16
+ --name "$WORKLOAD_IDENTITY_NAME" >/dev/null
17
+ fi
18
+
19
+ printf 'AgentCore workload identity deleted: %s\n' "$WORKLOAD_IDENTITY_NAME"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # THINK-324 — Twenty-side workload-identity guarantee. Create-if-missing
5
+ # ONLY: when the identity already exists (created by the proof lifecycle or a
6
+ # prior run) this script must not touch it — reconcile_twenty_identity.sh
7
+ # owns the return-url allowlist and always runs afterwards.
8
+
9
+ : "${AWS_REGION:?AWS_REGION is required}"
10
+ : "${WORKLOAD_IDENTITY_NAME:?WORKLOAD_IDENTITY_NAME is required}"
11
+ : "${OAUTH_RETURN_URLS_JSON:?OAUTH_RETURN_URLS_JSON is required}"
12
+
13
+ jq -e 'type == "array" and length > 0 and all(.[]; type == "string" and length > 0)' \
14
+ <<<"$OAUTH_RETURN_URLS_JSON" >/dev/null
15
+
16
+ if aws bedrock-agentcore-control get-workload-identity \
17
+ --region "$AWS_REGION" \
18
+ --name "$WORKLOAD_IDENTITY_NAME" >/dev/null 2>&1; then
19
+ printf 'AgentCore workload identity already present: %s\n' "$WORKLOAD_IDENTITY_NAME"
20
+ exit 0
21
+ fi
22
+
23
+ aws bedrock-agentcore-control create-workload-identity \
24
+ --region "$AWS_REGION" \
25
+ --name "$WORKLOAD_IDENTITY_NAME" \
26
+ --allowed-resource-oauth2-return-urls "$OAUTH_RETURN_URLS_JSON" \
27
+ --tags '{"purpose":"twenty-user-federation","managed-by":"terraform"}' >/dev/null
28
+
29
+ printf 'AgentCore workload identity created: %s\n' "$WORKLOAD_IDENTITY_NAME"
@@ -6,19 +6,32 @@ region="$(jq -r '.region // empty' <<<"$input")"
6
6
  workload_name="$(jq -r '.workload_identity_name // empty' <<<"$input")"
7
7
  provider_name="$(jq -r '.credential_provider_name // empty' <<<"$input")"
8
8
  twenty_provider_name="$(jq -r '.twenty_credential_provider_name // empty' <<<"$input")"
9
- if [[ -z "$region" || -z "$workload_name" || -z "$provider_name" || -z "$twenty_provider_name" ]]; then
10
- printf '%s\n' '{"error":"region and identity names are required"}' >&2
9
+ # THINK-324: an empty provider name means that half is disabled (proof
10
+ # retired, or Twenty absent on a proof-only stage) — skip its reads and
11
+ # return empty fields instead of failing.
12
+ if [[ -z "$region" || -z "$workload_name" ]]; then
13
+ printf '%s\n' '{"error":"region and workload_identity_name are required"}' >&2
11
14
  exit 1
12
15
  fi
13
16
 
14
17
  workload_json="$(aws bedrock-agentcore-control get-workload-identity \
15
18
  --region "$region" --name "$workload_name" --output json)"
16
- provider_json="$(aws bedrock-agentcore-control get-oauth2-credential-provider \
17
- --region "$region" --name "$provider_name" --output json)"
18
- twenty_provider_json="$(aws bedrock-agentcore-control get-oauth2-credential-provider \
19
- --region "$region" --name "$twenty_provider_name" --output json)"
19
+
20
+ provider_json='{}'
21
+ if [[ -n "$provider_name" ]]; then
22
+ provider_json="$(aws bedrock-agentcore-control get-oauth2-credential-provider \
23
+ --region "$region" --name "$provider_name" --output json)"
24
+ fi
25
+
26
+ twenty_provider_json='{}'
27
+ if [[ -n "$twenty_provider_name" ]]; then
28
+ twenty_provider_json="$(aws bedrock-agentcore-control get-oauth2-credential-provider \
29
+ --region "$region" --name "$twenty_provider_name" --output json)"
30
+ fi
20
31
 
21
32
  jq -n \
33
+ --arg proof_enabled "$provider_name" \
34
+ --arg twenty_enabled "$twenty_provider_name" \
22
35
  --arg workload_identity_arn "$(jq -r '.workloadIdentityArn // empty' <<<"$workload_json")" \
23
36
  --arg credential_provider_arn "$(jq -r '.credentialProviderArn // empty' <<<"$provider_json")" \
24
37
  --arg credential_secret_arn "$(jq -r '
@@ -29,7 +42,9 @@ jq -n \
29
42
  ' <<<"$provider_json")" \
30
43
  --arg twenty_credential_provider_arn "$(jq -r '.credentialProviderArn // empty' <<<"$twenty_provider_json")" \
31
44
  --arg twenty_oauth_callback_url "$(jq -r '.callbackUrl // empty' <<<"$twenty_provider_json")" \
32
- 'if ($workload_identity_arn == "" or $credential_provider_arn == "" or $credential_secret_arn == "" or $twenty_credential_provider_arn == "" or $twenty_oauth_callback_url == "")
45
+ 'if ($workload_identity_arn == ""
46
+ or ($proof_enabled != "" and ($credential_provider_arn == "" or $credential_secret_arn == ""))
47
+ or ($twenty_enabled != "" and ($twenty_credential_provider_arn == "" or $twenty_oauth_callback_url == "")))
33
48
  then error("AgentCore Identity readback omitted required ARNs")
34
49
  else {
35
50
  workload_identity_arn: $workload_identity_arn,
@@ -19,56 +19,27 @@ aws bedrock-agentcore-control update-workload-identity \
19
19
  --name "$WORKLOAD_IDENTITY_NAME" \
20
20
  --allowed-resource-oauth2-return-urls "$OAUTH_RETURN_URLS_JSON" >/dev/null
21
21
 
22
- client_record="$(aws secretsmanager get-secret-value \
23
- --region "$AWS_REGION" \
24
- --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
25
- --query SecretString \
26
- --output text 2>/dev/null || true)"
27
- if [[ -z "$client_record" ]]; then
28
- client_record='{}'
29
- fi
30
- client_id="$(jq -r '.client_id // empty' <<<"$client_record")"
31
- if [[ -z "$client_id" ]] ||
32
- ! jq -e '.client_secret | type == "string" and length > 0' \
33
- <<<"$client_record" >/dev/null 2>&1; then
34
- printf '%s\n' \
35
- 'Twenty confidential client is not bootstrapped; run bootstrap_twenty_oauth_client.sh with an authenticated administrator token secret' >&2
36
- exit 1
37
- fi
38
-
39
- provider_payload="$(jq -n \
40
- --arg region "$AWS_REGION" \
41
- --arg name "$TWENTY_CREDENTIAL_PROVIDER_NAME" \
42
- --arg issuer "$TWENTY_OAUTH_ISSUER" \
43
- --arg authorizationEndpoint "${TWENTY_OAUTH_ISSUER%/}/authorize" \
44
- --arg tokenEndpoint "${TWENTY_OAUTH_ISSUER%/}/oauth/token" \
45
- --arg clientId "$client_id" \
46
- --arg secretArn "$TWENTY_CLIENT_SECRET_ARN" \
47
- '{
48
- region: $region,
49
- name: $name,
50
- issuer: $issuer,
51
- authorizationEndpoint: $authorizationEndpoint,
52
- tokenEndpoint: $tokenEndpoint,
53
- clientId: $clientId,
54
- secretArn: $secretArn
55
- }')"
56
-
57
22
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
58
- provider_response="$(printf '%s' "$provider_payload" | \
59
- node "$script_dir/reconcile_twenty_provider.mjs")"
23
+ provider_response="$(bash "$script_dir/bootstrap_twenty_oauth_client.sh")"
60
24
  callback_url="$(jq -r '.callbackUrl // empty' <<<"$provider_response")"
61
25
  if [[ -z "$callback_url" || "$callback_url" == "None" ]]; then
62
26
  printf '%s\n' 'AgentCore Identity did not return the Twenty callback URL' >&2
63
27
  exit 1
64
28
  fi
65
29
 
66
- registered_callback="$(jq -r '.redirect_uris[0] // empty' <<<"$client_record")"
67
- if [[ "$registered_callback" != "$callback_url" ]]; then
68
- printf 'Twenty callback mismatch: registered=%s agentcore=%s\n' \
69
- "$registered_callback" "$callback_url" >&2
70
- exit 1
71
- fi
72
-
73
- printf 'AgentCore Twenty provider reconciled: workload=%s provider=%s resource=%s\n' \
74
- "$WORKLOAD_IDENTITY_NAME" "$TWENTY_CREDENTIAL_PROVIDER_NAME" "$TWENTY_OAUTH_RESOURCE"
30
+ client_record="$(aws secretsmanager get-secret-value \
31
+ --region "$AWS_REGION" \
32
+ --secret-id "$TWENTY_CLIENT_SECRET_ARN" \
33
+ --query SecretString \
34
+ --output text)"
35
+ jq -e --arg callback "$callback_url" \
36
+ '.bootstrap_state == "ready" and
37
+ (.client_id | type == "string" and length > 0) and
38
+ (.client_secret | type == "string" and length > 0) and
39
+ (.redirect_uris | type == "array" and index($callback) != null) and
40
+ .token_endpoint_auth_method == "client_secret_post"' \
41
+ <<<"$client_record" >/dev/null
42
+
43
+ printf 'AgentCore Twenty provider reconciled: workload=%s provider=%s resource=%s callback=%s\n' \
44
+ "$WORKLOAD_IDENTITY_NAME" "$TWENTY_CREDENTIAL_PROVIDER_NAME" \
45
+ "$TWENTY_OAUTH_RESOURCE" "$callback_url"
@@ -8,6 +8,20 @@ import {
8
8
  UpdateOauth2CredentialProviderCommand,
9
9
  } from "@aws-sdk/client-bedrock-agentcore-control";
10
10
 
11
+ if (process.argv[2] === "--runtime-preflight") {
12
+ process.stdout.write(
13
+ JSON.stringify({
14
+ entrypoint: "reconcile_twenty_provider",
15
+ sdkImportReady:
16
+ typeof BedrockAgentCoreControlClient === "function" &&
17
+ typeof CreateOauth2CredentialProviderCommand === "function" &&
18
+ typeof GetOauth2CredentialProviderCommand === "function" &&
19
+ typeof UpdateOauth2CredentialProviderCommand === "function",
20
+ }),
21
+ );
22
+ process.exit(0);
23
+ }
24
+
11
25
  const chunks = [];
12
26
  for await (const chunk of process.stdin) chunks.push(chunk);
13
27
  const input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -47,15 +61,63 @@ try {
47
61
  if (!(error instanceof ResourceNotFoundException)) throw error;
48
62
  }
49
63
 
50
- const response = exists
51
- ? await client.send(new UpdateOauth2CredentialProviderCommand(providerInput))
52
- : await client.send(new CreateOauth2CredentialProviderCommand(providerInput));
64
+ if (exists) {
65
+ await client.send(new UpdateOauth2CredentialProviderCommand(providerInput));
66
+ } else {
67
+ await client.send(new CreateOauth2CredentialProviderCommand(providerInput));
68
+ }
69
+
70
+ const sleep = (milliseconds) =>
71
+ new Promise((resolve) => setTimeout(resolve, milliseconds));
72
+
73
+ let response;
74
+ for (let attempt = 0; attempt < 30; attempt += 1) {
75
+ response = await client.send(
76
+ new GetOauth2CredentialProviderCommand({ name: input.name }),
77
+ );
78
+ const status = response.status;
79
+ const provider =
80
+ response.oauth2ProviderConfigOutput?.customOauth2ProviderConfig;
81
+ const secretArn =
82
+ typeof response.clientSecretArn === "string"
83
+ ? response.clientSecretArn
84
+ : response.clientSecretArn?.secretArn;
85
+ const ready =
86
+ response.callbackUrl &&
87
+ response.credentialProviderArn &&
88
+ provider?.clientId === input.clientId &&
89
+ secretArn === input.secretArn &&
90
+ response.clientSecretJsonKey === "client_secret" &&
91
+ response.clientSecretSource === "EXTERNAL" &&
92
+ status === "READY";
93
+ if (ready) break;
94
+ if (status?.endsWith("_FAILED")) {
95
+ throw new Error(`AgentCore OAuth provider ${input.name} entered ${status}`);
96
+ }
97
+ if (attempt === 29) {
98
+ throw new Error(
99
+ `AgentCore OAuth provider ${input.name} did not converge to the requested client and external secret`,
100
+ );
101
+ }
102
+ await sleep(2000);
103
+ }
104
+
105
+ const secretArn =
106
+ typeof response.clientSecretArn === "string"
107
+ ? response.clientSecretArn
108
+ : response.clientSecretArn?.secretArn;
109
+ const provider =
110
+ response.oauth2ProviderConfigOutput?.customOauth2ProviderConfig;
53
111
 
54
112
  process.stdout.write(
55
113
  JSON.stringify({
56
114
  name: response.name,
57
115
  callbackUrl: response.callbackUrl,
58
116
  credentialProviderArn: response.credentialProviderArn,
117
+ clientId: provider?.clientId,
118
+ clientSecretArn: secretArn,
119
+ clientSecretJsonKey: response.clientSecretJsonKey,
59
120
  clientSecretSource: response.clientSecretSource,
121
+ status: response.status,
60
122
  }),
61
123
  );
@@ -0,0 +1,15 @@
1
+ terraform {
2
+ required_version = "= 1.8.5"
3
+ }
4
+
5
+ removed {
6
+ from = module.thinkwork.module.agentcore_proof_identity.terraform_data.twenty_identity_lifecycle
7
+
8
+ lifecycle {
9
+ destroy = false
10
+ }
11
+ }
12
+
13
+ module "thinkwork" {
14
+ source = "./thinkwork"
15
+ }
@@ -0,0 +1,7 @@
1
+ terraform {
2
+ required_version = "= 1.8.5"
3
+ }
4
+
5
+ module "thinkwork" {
6
+ source = "./thinkwork"
7
+ }