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
@@ -1,359 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Smoke test the Twenty CRM managed application deployment.
4
- *
5
- * Dry-run is the default. Set SMOKE_ENABLE_TWENTY_MANAGED_APP=1 after deploy
6
- * to read Terraform/API deployment status and probe the public Twenty /healthz
7
- * endpoint when the runtime is enabled.
8
- *
9
- * Optional live env:
10
- * SMOKE_TERRAFORM_DIR=terraform/examples/greenfield
11
- * SMOKE_TWENTY_URL=https://crm.example.com
12
- * SMOKE_TENANT_ID=<tenant-id>
13
- * VITE_GRAPHQL_HTTP_URL, GRAPHQL_HTTP_URL, or API_GRAPHQL_URL
14
- * API_AUTH_SECRET, THINKWORK_API_SECRET, VITE_GRAPHQL_API_KEY, or GRAPHQL_API_KEY
15
- */
16
-
17
- import { execFileSync } from "node:child_process";
18
- import fs from "node:fs";
19
- import path from "node:path";
20
- import process from "node:process";
21
- import { attachSmokeEvidence } from "../../../scripts/smoke/deployment-evidence.mjs";
22
-
23
- const LIVE_ENABLED = process.env.SMOKE_ENABLE_TWENTY_MANAGED_APP === "1";
24
- const TIMEOUT_MS = Number(process.env.SMOKE_TIMEOUT_MS || 15_000);
25
-
26
- const env = {
27
- ...loadEnvFile(),
28
- ...process.env,
29
- };
30
-
31
- const terraformDir = env.SMOKE_TERRAFORM_DIR || "terraform/examples/greenfield";
32
- const apiUrl = first(
33
- env.VITE_GRAPHQL_HTTP_URL,
34
- env.GRAPHQL_HTTP_URL,
35
- env.API_GRAPHQL_URL,
36
- );
37
- const apiSecret = first(env.API_AUTH_SECRET, env.THINKWORK_API_SECRET);
38
- const apiKey = first(
39
- env.VITE_GRAPHQL_API_KEY,
40
- env.APPSYNC_API_KEY,
41
- env.GRAPHQL_API_KEY,
42
- );
43
- const tenantId = first(env.SMOKE_TENANT_ID, env.TENANT_ID);
44
-
45
- if (!LIVE_ENABLED) {
46
- console.log(
47
- JSON.stringify(
48
- await attachSmokeEvidence(
49
- "twenty-managed-app",
50
- {
51
- ok: true,
52
- skippedLive: true,
53
- reason:
54
- "set SMOKE_ENABLE_TWENTY_MANAGED_APP=1 to run the deployed Twenty managed-app smoke",
55
- dryRun: {
56
- terraformDir,
57
- requiredWhenRunning: [
58
- "Terraform outputs in SMOKE_TERRAFORM_DIR or SMOKE_TWENTY_URL",
59
- "Twenty provisioned/runtime deployment status",
60
- ],
61
- optionalGraphqlEnv: [
62
- "SMOKE_TENANT_ID",
63
- "VITE_GRAPHQL_HTTP_URL or GRAPHQL_HTTP_URL or API_GRAPHQL_URL",
64
- "API_AUTH_SECRET or THINKWORK_API_SECRET or VITE_GRAPHQL_API_KEY or GRAPHQL_API_KEY",
65
- ],
66
- verifies: [
67
- "twenty_provisioned false skips cleanly",
68
- "twenty_runtime_enabled true requires HTTPS twenty_url",
69
- "public Twenty /healthz responds successfully",
70
- "optional GraphQL deployment status and managed-app health agree with the public URL",
71
- ],
72
- },
73
- },
74
- env,
75
- ),
76
- null,
77
- 2,
78
- ),
79
- );
80
- process.exit(0);
81
- }
82
-
83
- try {
84
- const result = await runLiveSmoke();
85
- console.log(
86
- JSON.stringify(
87
- await attachSmokeEvidence(
88
- "twenty-managed-app",
89
- { ok: true, ...result },
90
- env,
91
- ),
92
- null,
93
- 2,
94
- ),
95
- );
96
- } catch (error) {
97
- fail(error instanceof Error ? error.message : String(error));
98
- }
99
-
100
- async function runLiveSmoke() {
101
- const terraform = readTerraformOutputs(terraformDir);
102
- const graphql = await readGraphqlStatus().catch((error) => ({
103
- error: error instanceof Error ? error.message : String(error),
104
- }));
105
- const status = resolveStatus({ terraform, graphql });
106
-
107
- if (!status.provisioned) {
108
- return {
109
- skippedLive: true,
110
- reason: "Twenty CRM is not provisioned for this stage.",
111
- terraform,
112
- graphql,
113
- };
114
- }
115
-
116
- if (!status.runtimeEnabled) {
117
- return {
118
- skippedLive: true,
119
- reason: "Twenty CRM is provisioned but runtime is parked.",
120
- status,
121
- terraform,
122
- graphql,
123
- };
124
- }
125
-
126
- if (!status.url) {
127
- throw new Error(
128
- "Twenty runtime is enabled but no public URL was resolved.",
129
- );
130
- }
131
- if (!status.url.startsWith("https://")) {
132
- throw new Error(`Twenty URL must be HTTPS, got ${status.url}.`);
133
- }
134
-
135
- const publicHealth = await probeHealthz(status.url);
136
- const graphqlHealth =
137
- graphql?.deploymentStatus && !graphql.error
138
- ? await readGraphqlHealth().catch((error) => ({
139
- error: error instanceof Error ? error.message : String(error),
140
- }))
141
- : null;
142
-
143
- if (!publicHealth.healthy) {
144
- throw new Error(
145
- `Twenty /healthz failed: HTTP ${publicHealth.statusCode ?? "none"} ${publicHealth.message}`,
146
- );
147
- }
148
- if (graphqlHealth && "healthy" in graphqlHealth && !graphqlHealth.healthy) {
149
- throw new Error(
150
- `GraphQL managed health reported unhealthy: ${graphqlHealth.message}`,
151
- );
152
- }
153
-
154
- return {
155
- status,
156
- publicHealth,
157
- graphqlHealth,
158
- terraform,
159
- graphql,
160
- };
161
- }
162
-
163
- function resolveStatus({ terraform, graphql }) {
164
- const app = graphql?.deploymentStatus?.managedApplications?.find(
165
- (entry) => entry.key === "twenty",
166
- );
167
- return {
168
- provisioned: bool(
169
- firstDefined(
170
- app?.provisioned,
171
- terraform.twenty_provisioned,
172
- !!env.SMOKE_TWENTY_URL,
173
- ),
174
- ),
175
- runtimeEnabled: bool(
176
- firstDefined(
177
- app?.runtimeEnabled,
178
- terraform.twenty_runtime_enabled,
179
- !!env.SMOKE_TWENTY_URL,
180
- ),
181
- ),
182
- url: first(env.SMOKE_TWENTY_URL, app?.url, terraform.twenty_url),
183
- source: app ? "graphql" : terraform.available ? "terraform" : "env",
184
- status: app?.status ?? null,
185
- message: app?.message ?? null,
186
- };
187
- }
188
-
189
- async function probeHealthz(baseUrl) {
190
- const endpoint = new URL("/healthz", baseUrl).toString();
191
- const started = Date.now();
192
- const controller = new AbortController();
193
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
194
- try {
195
- const response = await fetch(endpoint, { signal: controller.signal });
196
- const body = await response.text().catch(() => "");
197
- return {
198
- endpoint,
199
- healthy: response.ok,
200
- statusCode: response.status,
201
- latencyMs: Date.now() - started,
202
- message: body.slice(0, 300),
203
- };
204
- } finally {
205
- clearTimeout(timer);
206
- }
207
- }
208
-
209
- function readTerraformOutputs(dir) {
210
- const resolved = path.resolve(dir);
211
- if (!fs.existsSync(resolved)) {
212
- return { available: false, reason: `Terraform dir not found: ${dir}` };
213
- }
214
-
215
- try {
216
- const raw = execFileSync(
217
- "terraform",
218
- ["-chdir=" + resolved, "output", "-json"],
219
- {
220
- encoding: "utf8",
221
- stdio: ["ignore", "pipe", "pipe"],
222
- },
223
- );
224
- const parsed = JSON.parse(raw);
225
- return {
226
- available: true,
227
- twenty_provisioned: outputValue(parsed.twenty_provisioned),
228
- twenty_runtime_enabled: outputValue(parsed.twenty_runtime_enabled),
229
- twenty_url: outputValue(parsed.twenty_url),
230
- twenty_server_log_group_name: outputValue(
231
- parsed.twenty_server_log_group_name,
232
- ),
233
- twenty_worker_log_group_name: outputValue(
234
- parsed.twenty_worker_log_group_name,
235
- ),
236
- twenty_cache_endpoint: outputValue(parsed.twenty_cache_endpoint),
237
- };
238
- } catch (error) {
239
- return {
240
- available: false,
241
- reason: error instanceof Error ? error.message : String(error),
242
- };
243
- }
244
- }
245
-
246
- async function readGraphqlStatus() {
247
- if (!apiUrl || (!apiSecret && !apiKey)) return null;
248
- const data = await gql(`
249
- query TwentySmokeDeploymentStatus {
250
- deploymentStatus {
251
- stage
252
- region
253
- twentyProvisioned
254
- twentyRuntimeEnabled
255
- twentyUrl
256
- managedApplications {
257
- key
258
- status
259
- provisioned
260
- runtimeEnabled
261
- url
262
- message
263
- }
264
- }
265
- }
266
- `);
267
- return data;
268
- }
269
-
270
- async function readGraphqlHealth() {
271
- const data = await gql(`
272
- query TwentySmokeManagedApplicationHealth {
273
- managedApplicationHealthCheck(key: "twenty") {
274
- healthy
275
- statusCode
276
- latencyMs
277
- endpoint
278
- message
279
- }
280
- }
281
- `);
282
- return data.managedApplicationHealthCheck;
283
- }
284
-
285
- async function gql(query) {
286
- const headers = { "content-type": "application/json" };
287
- if (tenantId) headers["x-tenant-id"] = tenantId;
288
- if (apiSecret) headers.authorization = `Bearer ${apiSecret}`;
289
- else headers["x-api-key"] = apiKey;
290
-
291
- const response = await fetch(apiUrl, {
292
- method: "POST",
293
- headers,
294
- body: JSON.stringify({ query }),
295
- });
296
- if (!response.ok) {
297
- throw new Error(
298
- `GraphQL HTTP ${response.status}: ${await response.text()}`,
299
- );
300
- }
301
- const body = await response.json();
302
- if (body.errors?.length) {
303
- throw new Error(`GraphQL errors: ${JSON.stringify(body.errors)}`);
304
- }
305
- return body.data;
306
- }
307
-
308
- function outputValue(entry) {
309
- return entry && Object.prototype.hasOwnProperty.call(entry, "value")
310
- ? entry.value
311
- : undefined;
312
- }
313
-
314
- function firstDefined(...values) {
315
- return values.find((value) => value !== undefined && value !== null);
316
- }
317
-
318
- function bool(value) {
319
- return value === true || value === "true" || value === "1";
320
- }
321
-
322
- function loadEnvFile() {
323
- const explicit = process.env.COMPUTER_ENV_FILE;
324
- if (explicit === "none") return {};
325
- const candidates = [explicit, "apps/web/.env", ".env"].filter(Boolean);
326
-
327
- for (const candidate of candidates) {
328
- const resolved = path.resolve(candidate);
329
- if (!fs.existsSync(resolved)) continue;
330
- return Object.fromEntries(
331
- fs
332
- .readFileSync(resolved, "utf8")
333
- .split(/\r?\n/)
334
- .map((line) => line.trim())
335
- .filter((line) => line && !line.startsWith("#") && line.includes("="))
336
- .map((line) => {
337
- const index = line.indexOf("=");
338
- const key = line.slice(0, index).trim();
339
- const value = line
340
- .slice(index + 1)
341
- .trim()
342
- .replace(/^['"]|['"]$/g, "");
343
- return [key, value];
344
- }),
345
- );
346
- }
347
- return {};
348
- }
349
-
350
- function first(...values) {
351
- return values.find(
352
- (value) => value !== undefined && value !== null && value !== "",
353
- );
354
- }
355
-
356
- function fail(message) {
357
- console.error(JSON.stringify({ ok: false, error: message }, null, 2));
358
- process.exit(1);
359
- }
@@ -1,61 +0,0 @@
1
- # Sandbox base image for the AgentCore Code Interpreter.
2
- #
3
- # Builds a Python 3.12 container with a small, pinned set of libraries
4
- # template authors can rely on without paying the first-turn `pip install`
5
- # tax. Installs sitecustomize.py so the stdout/stderr token redactor is
6
- # active before any user code or framework traceback runs on cold start.
7
- #
8
- # Image tagging is CI-driven (see scripts/build_and_push_sandbox_base.sh).
9
- # Tags are immutable; a bump ships as a reviewable PR.
10
- #
11
- # Build context is the repository root so the COPY line can reach the
12
- # module-owned Python source below.
13
- #
14
- # docker build -f terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base .
15
-
16
- FROM python:3.12-slim AS base
17
-
18
- # ---------------------------------------------------------------------------
19
- # System packages — minimal. The sandbox runs agent code, not a full distro.
20
- # ---------------------------------------------------------------------------
21
- RUN apt-get update && apt-get install -y --no-install-recommends \
22
- ca-certificates \
23
- curl \
24
- git \
25
- && rm -rf /var/lib/apt/lists/*
26
-
27
- # ---------------------------------------------------------------------------
28
- # Blessed Python libraries — pinned to explicit versions per plan R14/R24.
29
- # Bumps happen in reviewable PRs. Runtime `pip install` is still allowed
30
- # (per R15; T2 residual is named), but these baseline packages install once
31
- # at image bake and don't pay the per-turn cost.
32
- # ---------------------------------------------------------------------------
33
- RUN pip install --no-cache-dir \
34
- pandas==2.2.3 \
35
- numpy==2.1.3 \
36
- requests==2.32.3 \
37
- boto3==1.38.0 \
38
- httpx==0.28.1 \
39
- pyyaml==6.0.2 \
40
- openpyxl==3.1.5 \
41
- python-dateutil==2.9.0.post0
42
-
43
- # ---------------------------------------------------------------------------
44
- # sitecustomize.py — the R13 stdout/stderr token redactor. Python
45
- # auto-imports any module named "sitecustomize" on interpreter startup, so
46
- # placing it in site-packages makes the redactor load before any user code
47
- # (and before the framework traceback on cold start). See the module
48
- # docstring for the honestly-scoped invariant and named residual gaps.
49
- # ---------------------------------------------------------------------------
50
- COPY terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py \
51
- /usr/local/lib/python3.12/site-packages/sitecustomize.py
52
-
53
- # ---------------------------------------------------------------------------
54
- # Startup assertion — fail loudly if the module isn't wired up or
55
- # THINKWORK_SANDBOX_SCRUBBER=off slips through to prod. Runs in the same
56
- # image smoke-test CI job that pushes the tag.
57
- # ---------------------------------------------------------------------------
58
- RUN python -c "import sitecustomize; assert sitecustomize.installed(), 'sitecustomize did not install its stdio wrapper on import'; print('sitecustomize: OK')"
59
-
60
- # AgentCore Code Interpreter supplies its own entrypoint; we provide the
61
- # libraries + scrubber only. No CMD/ENTRYPOINT override.
@@ -1,227 +0,0 @@
1
- """sitecustomize — primary R13 token redactor for the AgentCore sandbox base image.
2
-
3
- Python auto-imports any module named ``sitecustomize`` during interpreter
4
- startup (before user code, before the preamble, before framework tracebacks on
5
- cold start). This file installs a stdio write-wrapper keyed on a session-scoped
6
- set of token values so no token leaks to stdout / stderr via Python-stdio
7
- writes.
8
-
9
- The invariant is honestly scoped per the brainstorm's R13 (as aligned in
10
- docs/brainstorms/2026-04-22-agentcore-code-sandbox-requirements.md):
11
-
12
- "no token value reaches a persisted log ... via Python-stdio-mediated writes"
13
-
14
- Bytes that bypass Python stdio (``os.write(fd, ...)``, subprocesses inheriting
15
- fds, C extensions, multiprocessing children) are the named residual class
16
- covered by the Unit 12 CloudWatch subscription-filter backstop for known
17
- OAuth prefixes, not by this module.
18
-
19
- Public API, callable from the sandbox preamble:
20
- register_token(value: str) -> None
21
- register_tokens(values: Iterable[str]) -> None
22
- clear_tokens() -> None
23
- installed() -> bool
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- import base64
29
- import binascii
30
- import os
31
- import sys
32
- import urllib.parse
33
- from collections.abc import Iterable
34
- from typing import Any
35
-
36
- # ---------------------------------------------------------------------------
37
- # Module state
38
- # ---------------------------------------------------------------------------
39
-
40
- # Every unique byte-sequence we redact. Populated by ``register_token`` — each
41
- # registered token expands into its cleartext, standard base64, URL-safe
42
- # base64, URL-encoded, and hex forms (only the forms whose length is at least
43
- # MIN_TOKEN_LEN, to avoid redacting short accidental matches like "AQ").
44
- _redactions: set[bytes] = set()
45
-
46
- # Redaction placeholder. Length unimportant — stream layout survives because
47
- # writes stay synchronous.
48
- _REDACTION = b"<redacted>"
49
-
50
- # Forms shorter than this are rejected — redacting 3-character strings is the
51
- # shape of a wildcard bug, not a security control.
52
- MIN_TOKEN_LEN = 12
53
-
54
- # Rolling-suffix buffer size. Set at ``register_token`` time to
55
- # max(existing, 2 * len(new_token_form)). The wrapper keeps this many bytes
56
- # from the previous write and prepends them to the current write before
57
- # searching, so a token split across two ``write()`` calls is caught as long
58
- # as neither split half exceeds the buffer. Adversarial fragmentation at
59
- # arbitrary positions is a named residual (see R13).
60
- _rolling_buffer_size: int = 0
61
-
62
-
63
- def _expand(value: str) -> list[bytes]:
64
- """Return the encoded forms of a token value we treat as equivalent.
65
-
66
- Each form is added to the redaction set so ``print(base64.b64encode(token))``
67
- and the cleartext ``print(token)`` are both scrubbed.
68
- """
69
- if not value:
70
- return []
71
- raw = value.encode("utf-8")
72
- forms: list[bytes] = []
73
-
74
- def maybe_add(candidate: bytes) -> None:
75
- if len(candidate) >= MIN_TOKEN_LEN:
76
- forms.append(candidate)
77
-
78
- maybe_add(raw)
79
- maybe_add(base64.b64encode(raw).rstrip(b"="))
80
- maybe_add(base64.b64encode(raw))
81
- maybe_add(base64.urlsafe_b64encode(raw).rstrip(b"="))
82
- maybe_add(base64.urlsafe_b64encode(raw))
83
- maybe_add(urllib.parse.quote(value, safe="").encode("ascii"))
84
- maybe_add(binascii.hexlify(raw))
85
- return forms
86
-
87
-
88
- def register_token(value: str) -> None:
89
- """Add a token (and its derived encodings) to the redaction set.
90
-
91
- Safe to call multiple times with the same value. No-op for empty values.
92
- """
93
- global _rolling_buffer_size
94
- for form in _expand(value):
95
- _redactions.add(form)
96
- required = 2 * len(form)
97
- if required > _rolling_buffer_size:
98
- _rolling_buffer_size = required
99
-
100
-
101
- def register_tokens(values: Iterable[str]) -> None:
102
- for v in values:
103
- register_token(v)
104
-
105
-
106
- def clear_tokens() -> None:
107
- """Drop every registered token. Called at session teardown by the container."""
108
- global _rolling_buffer_size
109
- _redactions.clear()
110
- _rolling_buffer_size = 0
111
-
112
-
113
- # ---------------------------------------------------------------------------
114
- # Stdio wrapper
115
- # ---------------------------------------------------------------------------
116
-
117
-
118
- def _redact(chunk: bytes, tail_buffer: bytearray) -> tuple[bytes, bytearray]:
119
- """Return (scrubbed_output_chunk, new_tail_buffer).
120
-
121
- The tail buffer holds the trailing N bytes of the previous write so we can
122
- catch tokens split across write boundaries. The output we emit is the part
123
- of ``tail_buffer + chunk`` that is safely past the trailing window; the
124
- suffix stays in the new tail for the next call.
125
- """
126
- if not _redactions:
127
- return chunk, tail_buffer
128
-
129
- combined = bytes(tail_buffer) + chunk
130
- for form in _redactions:
131
- combined = combined.replace(form, _REDACTION)
132
-
133
- if _rolling_buffer_size and len(combined) > _rolling_buffer_size:
134
- emit = combined[:-_rolling_buffer_size]
135
- new_tail = bytearray(combined[-_rolling_buffer_size:])
136
- else:
137
- emit = b""
138
- new_tail = bytearray(combined)
139
-
140
- return emit, new_tail
141
-
142
-
143
- class _RedactingStream:
144
- """Wraps sys.stdout / sys.stderr's underlying buffer with a redactor.
145
-
146
- Intercepts both ``write(str)`` (TextIOBase) and ``.buffer.write(bytes)``
147
- paths so ``print(...)``, ``sys.stdout.write(...)``, and
148
- ``sys.stdout.buffer.write(...)`` all flow through the scrubber.
149
- """
150
-
151
- def __init__(self, wrapped: Any, name: str) -> None:
152
- self._wrapped = wrapped
153
- self._name = name
154
- self._tail = bytearray()
155
- # AgentCore writes to stdout/stderr line-buffered; we flush our tail
156
- # on every newline-terminated write so output appears promptly.
157
-
158
- def write(self, data: Any) -> int:
159
- if isinstance(data, str):
160
- payload = data.encode("utf-8", errors="replace")
161
- wrote_str = True
162
- elif isinstance(data, (bytes, bytearray, memoryview)):
163
- payload = bytes(data)
164
- wrote_str = False
165
- else:
166
- # Defer unknown types to the wrapped object — let it raise.
167
- return self._wrapped.write(data)
168
-
169
- emit, self._tail = _redact(payload, self._tail)
170
- # A newline forces a tail flush so output doesn't lag arbitrarily when
171
- # no new writes arrive. This is a UX concession, not a security one —
172
- # redaction is complete whether we flush or not.
173
- if b"\n" in payload:
174
- emit = emit + bytes(self._tail)
175
- self._tail = bytearray()
176
-
177
- if not emit:
178
- return len(data) if wrote_str else len(payload)
179
-
180
- if wrote_str:
181
- self._wrapped.write(emit.decode("utf-8", errors="replace"))
182
- else:
183
- # TextIOWrapper.write raises on bytes; prefer the underlying buffer.
184
- buf = getattr(self._wrapped, "buffer", None)
185
- if buf is not None:
186
- buf.write(emit)
187
- else:
188
- self._wrapped.write(emit)
189
- return len(data) if wrote_str else len(payload)
190
-
191
- def flush(self) -> None:
192
- if self._tail:
193
- data = bytes(self._tail)
194
- self._tail = bytearray()
195
- buf = getattr(self._wrapped, "buffer", None)
196
- if buf is not None:
197
- buf.write(data)
198
- else:
199
- self._wrapped.write(data.decode("utf-8", errors="replace"))
200
- self._wrapped.flush()
201
-
202
- # Forward any other attribute access (fileno, isatty, encoding, etc.) so
203
- # libraries that introspect the stream still see what they expect.
204
- def __getattr__(self, item: str) -> Any:
205
- return getattr(self._wrapped, item)
206
-
207
-
208
- _installed = False
209
-
210
-
211
- def installed() -> bool:
212
- return _installed
213
-
214
-
215
- def _install() -> None:
216
- global _installed
217
- if _installed:
218
- return
219
- sys.stdout = _RedactingStream(sys.stdout, "stdout")
220
- sys.stderr = _RedactingStream(sys.stderr, "stderr")
221
- _installed = True
222
-
223
-
224
- # Install unconditionally on import. Disable only for a pytest diagnostic
225
- # marker the test suite can opt out of (set THINKWORK_SANDBOX_SCRUBBER=off).
226
- if os.environ.get("THINKWORK_SANDBOX_SCRUBBER", "on") != "off":
227
- _install()