thinkwork-cli 0.5.4 → 0.6.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.
@@ -33,6 +33,12 @@ variable "certificate_arn" {
33
33
  default = ""
34
34
  }
35
35
 
36
+ variable "is_spa" {
37
+ description = "When true, configure CloudFront for a single-page app: drop the directory-rewrite function and fall back to /index.html with 200 on 403/404 so the client router can handle deep links."
38
+ type = bool
39
+ default = false
40
+ }
41
+
36
42
  ################################################################################
37
43
  # S3 Bucket
38
44
  ################################################################################
@@ -74,6 +80,11 @@ resource "aws_cloudfront_origin_access_control" "site" {
74
80
  #
75
81
  # S3 with OAC doesn't auto-serve index.html for subdirectory requests.
76
82
  # /getting-started/ → /getting-started/index.html
83
+ #
84
+ # Always created (so flipping is_spa on an existing distribution doesn't hit
85
+ # CloudFront's "can't delete a function still associated with a distribution"
86
+ # error), but the viewer-request association is only wired up for non-SPA
87
+ # sites. For SPAs we rely on the 403/404 → /index.html fallback below.
77
88
  ################################################################################
78
89
 
79
90
  resource "aws_cloudfront_function" "rewrite" {
@@ -94,6 +105,19 @@ resource "aws_cloudfront_function" "rewrite" {
94
105
  EOF
95
106
  }
96
107
 
108
+ locals {
109
+ # For SPAs, 403/404 from S3 means "not a real asset" — serve index.html with
110
+ # a 200 so the client router can resolve the route. For directory-style
111
+ # static sites, surface a real 404 page.
112
+ error_responses = var.is_spa ? [
113
+ { error_code = 403, response_code = 200, response_page_path = "/index.html" },
114
+ { error_code = 404, response_code = 200, response_page_path = "/index.html" },
115
+ ] : [
116
+ { error_code = 404, response_code = 404, response_page_path = "/404.html" },
117
+ { error_code = 403, response_code = 404, response_page_path = "/404.html" },
118
+ ]
119
+ }
120
+
97
121
  ################################################################################
98
122
  # CloudFront Distribution
99
123
  ################################################################################
@@ -117,9 +141,12 @@ resource "aws_cloudfront_distribution" "site" {
117
141
  cached_methods = ["GET", "HEAD"]
118
142
  compress = true
119
143
 
120
- function_association {
121
- event_type = "viewer-request"
122
- function_arn = aws_cloudfront_function.rewrite.arn
144
+ dynamic "function_association" {
145
+ for_each = var.is_spa ? [] : [1]
146
+ content {
147
+ event_type = "viewer-request"
148
+ function_arn = aws_cloudfront_function.rewrite.arn
149
+ }
123
150
  }
124
151
 
125
152
  forwarded_values {
@@ -130,17 +157,13 @@ resource "aws_cloudfront_distribution" "site" {
130
157
  }
131
158
  }
132
159
 
133
- # Fallback for true 404s (e.g. deleted pages) — serve the 404 page
134
- custom_error_response {
135
- error_code = 404
136
- response_code = 404
137
- response_page_path = "/404.html"
138
- }
139
-
140
- custom_error_response {
141
- error_code = 403
142
- response_code = 404
143
- response_page_path = "/404.html"
160
+ dynamic "custom_error_response" {
161
+ for_each = local.error_responses
162
+ content {
163
+ error_code = custom_error_response.value.error_code
164
+ response_code = custom_error_response.value.response_code
165
+ response_page_path = custom_error_response.value.response_page_path
166
+ }
144
167
  }
145
168
 
146
169
  restrictions {
@@ -0,0 +1,39 @@
1
+ # www-dns
2
+
3
+ DNS + TLS wiring for the public website, using a Cloudflare-managed zone and an AWS ACM certificate.
4
+
5
+ ## What it creates
6
+
7
+ 1. ACM certificate in `us-east-1` covering the apex and `www` SAN, DNS-validated via Cloudflare records.
8
+ 2. Apex CNAME in Cloudflare pointing at the primary www CloudFront distribution (DNS-only, grey-cloud).
9
+ 3. S3 website-redirect bucket + a second CloudFront distribution that 301s `www.<domain>` → `https://<domain>`, plus its Cloudflare CNAME.
10
+
11
+ ## Why a second CloudFront distribution instead of a Cloudflare page rule
12
+
13
+ The apex and www records must be **DNS-only** so CloudFront can terminate TLS with the ACM cert. DNS-only traffic bypasses Cloudflare's proxy, so Cloudflare page rules and transform rules never run. The redirect has to live at AWS. An S3 website bucket with `redirect_all_requests_to` is the cheapest, simplest thing that works.
14
+
15
+ ## Required environment
16
+
17
+ - `CLOUDFLARE_API_TOKEN` — exported in the shell or CI environment. **Never** committed to tfvars. Rotate after anyone outside the deploy path sees it.
18
+
19
+ ## Usage (from greenfield example)
20
+
21
+ ```hcl
22
+ provider "cloudflare" {
23
+ # token read from CLOUDFLARE_API_TOKEN env var
24
+ }
25
+
26
+ module "www_dns" {
27
+ source = "../../modules/app/www-dns"
28
+ stage = var.stage
29
+ domain = var.www_domain
30
+ cloudflare_zone_id = var.cloudflare_zone_id
31
+ cloudfront_domain_name = module.thinkwork.www_distribution_domain
32
+ }
33
+ ```
34
+
35
+ Then pass `module.www_dns.certificate_arn` back into `module "thinkwork"` as `www_certificate_arn`.
36
+
37
+ ## First-apply ordering
38
+
39
+ On a fresh apply, Terraform has to create the primary CloudFront distribution (via `module.thinkwork.www_site`) before this module can reference its domain name. Terraform's dependency graph handles that automatically. If you're applying after a `terraform destroy`, two `apply` passes are fine — the first resolves the cert + distributions, the second binds the apex CNAME once the CloudFront distribution has deployed.
@@ -0,0 +1,245 @@
1
+ ################################################################################
2
+ # Public Website DNS + TLS (Cloudflare zone, AWS ACM cert, www→apex 301)
3
+ #
4
+ # Responsibilities:
5
+ # 1. ACM certificate in us-east-1 covering apex + www
6
+ # (+ optional docs + optional admin).
7
+ # 2. Cloudflare DNS records for ACM DNS validation.
8
+ # 3. Apex CNAME in Cloudflare → primary CloudFront distribution (DNS-only).
9
+ # 4. Second CloudFront distribution fronting an S3 website-redirect bucket
10
+ # that 301s www.<domain> → https://<domain>, plus its Cloudflare CNAME.
11
+ # 5. Optional docs.<domain> CNAME → docs CloudFront distribution.
12
+ # 6. Optional admin.<domain> CNAME → admin CloudFront distribution.
13
+ #
14
+ # Cloudflare records MUST be DNS-only (grey cloud). CloudFront terminates TLS
15
+ # with the ACM cert and needs the real Host header.
16
+ ################################################################################
17
+
18
+ terraform {
19
+ required_providers {
20
+ aws = {
21
+ source = "hashicorp/aws"
22
+ version = "~> 5.0"
23
+ }
24
+ cloudflare = {
25
+ source = "cloudflare/cloudflare"
26
+ version = "~> 4.0"
27
+ }
28
+ }
29
+ }
30
+
31
+ locals {
32
+ apex = var.domain
33
+ www = "www.${var.domain}"
34
+ docs = "docs.${var.domain}"
35
+ admin = "admin.${var.domain}"
36
+ name_id = replace(var.domain, ".", "-")
37
+
38
+ # ACM SANs: always include www, conditionally include docs and admin.
39
+ # Gated on plain bool vars (not on CloudFront outputs) to keep the
40
+ # dependency graph acyclic — distributions depend on the cert, so
41
+ # the cert mustn't depend on distribution outputs.
42
+ cert_sans = concat(
43
+ [local.www],
44
+ var.include_docs ? [local.docs] : [],
45
+ var.include_admin ? [local.admin] : [],
46
+ )
47
+
48
+ # CNAME records can only be created when we have the distribution
49
+ # domain to point at. Those inputs come after the cert is done, so
50
+ # they don't participate in the cert's dependency graph.
51
+ create_docs_record = var.include_docs && var.docs_cloudfront_domain_name != ""
52
+ create_admin_record = var.include_admin && var.admin_cloudfront_domain_name != ""
53
+ }
54
+
55
+ ################################################################################
56
+ # ACM certificate (us-east-1, covers apex + www [+ docs])
57
+ ################################################################################
58
+
59
+ resource "aws_acm_certificate" "www" {
60
+ domain_name = local.apex
61
+ subject_alternative_names = local.cert_sans
62
+ validation_method = "DNS"
63
+
64
+ lifecycle {
65
+ create_before_destroy = true
66
+ }
67
+
68
+ tags = {
69
+ Name = "thinkwork-${var.stage}-${local.name_id}"
70
+ }
71
+ }
72
+
73
+ resource "cloudflare_record" "acm_validation" {
74
+ for_each = {
75
+ for dvo in aws_acm_certificate.www.domain_validation_options : dvo.domain_name => {
76
+ name = dvo.resource_record_name
77
+ value = dvo.resource_record_value
78
+ type = dvo.resource_record_type
79
+ }
80
+ }
81
+
82
+ zone_id = var.cloudflare_zone_id
83
+ name = trimsuffix(each.value.name, ".")
84
+ content = trimsuffix(each.value.value, ".")
85
+ type = each.value.type
86
+ ttl = 60
87
+ proxied = false
88
+ comment = "ACM DNS validation for ${each.key}"
89
+ }
90
+
91
+ resource "aws_acm_certificate_validation" "www" {
92
+ certificate_arn = aws_acm_certificate.www.arn
93
+ validation_record_fqdns = [for r in cloudflare_record.acm_validation : r.hostname]
94
+ }
95
+
96
+ ################################################################################
97
+ # Apex DNS → primary CloudFront distribution
98
+ #
99
+ # Cloudflare flattens apex CNAMEs automatically. proxied=false is required so
100
+ # CloudFront sees the real Host header and the ACM cert matches.
101
+ ################################################################################
102
+
103
+ resource "cloudflare_record" "apex" {
104
+ zone_id = var.cloudflare_zone_id
105
+ name = local.apex
106
+ content = var.cloudfront_domain_name
107
+ type = "CNAME"
108
+ ttl = 300
109
+ proxied = false
110
+ comment = "thinkwork-${var.stage} apex → CloudFront (www)"
111
+ }
112
+
113
+ ################################################################################
114
+ # www.<domain> → apex 301 redirect
115
+ #
116
+ # Uses an S3 website-redirect bucket (website endpoint, not REST). Fronted by
117
+ # its own CloudFront distribution with the www alias and the same ACM cert.
118
+ ################################################################################
119
+
120
+ resource "aws_s3_bucket" "www_redirect" {
121
+ bucket = "thinkwork-${var.stage}-www-redirect"
122
+
123
+ tags = {
124
+ Name = "thinkwork-${var.stage}-www-redirect"
125
+ }
126
+ }
127
+
128
+ resource "aws_s3_bucket_public_access_block" "www_redirect" {
129
+ bucket = aws_s3_bucket.www_redirect.id
130
+
131
+ block_public_acls = true
132
+ block_public_policy = true
133
+ ignore_public_acls = true
134
+ restrict_public_buckets = true
135
+ }
136
+
137
+ resource "aws_s3_bucket_website_configuration" "www_redirect" {
138
+ bucket = aws_s3_bucket.www_redirect.id
139
+
140
+ redirect_all_requests_to {
141
+ host_name = local.apex
142
+ protocol = "https"
143
+ }
144
+ }
145
+
146
+ resource "aws_cloudfront_distribution" "www_redirect" {
147
+ enabled = true
148
+ aliases = [local.www]
149
+ price_class = "PriceClass_100"
150
+ is_ipv6_enabled = true
151
+ comment = "thinkwork-${var.stage}-www-redirect → ${local.apex}"
152
+
153
+ origin {
154
+ domain_name = aws_s3_bucket_website_configuration.www_redirect.website_endpoint
155
+ origin_id = "s3-website-${aws_s3_bucket.www_redirect.id}"
156
+
157
+ custom_origin_config {
158
+ http_port = 80
159
+ https_port = 443
160
+ origin_protocol_policy = "http-only"
161
+ origin_ssl_protocols = ["TLSv1.2"]
162
+ }
163
+ }
164
+
165
+ default_cache_behavior {
166
+ target_origin_id = "s3-website-${aws_s3_bucket.www_redirect.id}"
167
+ viewer_protocol_policy = "redirect-to-https"
168
+ allowed_methods = ["GET", "HEAD"]
169
+ cached_methods = ["GET", "HEAD"]
170
+ compress = true
171
+
172
+ forwarded_values {
173
+ query_string = false
174
+ cookies {
175
+ forward = "none"
176
+ }
177
+ }
178
+
179
+ min_ttl = 0
180
+ default_ttl = 3600
181
+ max_ttl = 86400
182
+ }
183
+
184
+ restrictions {
185
+ geo_restriction {
186
+ restriction_type = "none"
187
+ }
188
+ }
189
+
190
+ viewer_certificate {
191
+ acm_certificate_arn = aws_acm_certificate_validation.www.certificate_arn
192
+ ssl_support_method = "sni-only"
193
+ minimum_protocol_version = "TLSv1.2_2021"
194
+ }
195
+
196
+ tags = {
197
+ Name = "thinkwork-${var.stage}-www-redirect"
198
+ }
199
+ }
200
+
201
+ resource "cloudflare_record" "www_redirect" {
202
+ zone_id = var.cloudflare_zone_id
203
+ name = local.www
204
+ content = aws_cloudfront_distribution.www_redirect.domain_name
205
+ type = "CNAME"
206
+ ttl = 300
207
+ proxied = false
208
+ comment = "thinkwork-${var.stage} www → redirect distribution"
209
+ }
210
+
211
+ ################################################################################
212
+ # docs.<domain> → docs CloudFront distribution (optional)
213
+ #
214
+ # Created only when the greenfield stack wires docs_cloudfront_domain_name.
215
+ # The docs CloudFront alias picks up the same ACM cert via certificate_arn
216
+ # on the thinkwork module's docs_site.
217
+ ################################################################################
218
+
219
+ resource "cloudflare_record" "docs" {
220
+ count = local.create_docs_record ? 1 : 0
221
+
222
+ zone_id = var.cloudflare_zone_id
223
+ name = local.docs
224
+ content = var.docs_cloudfront_domain_name
225
+ type = "CNAME"
226
+ ttl = 300
227
+ proxied = false
228
+ comment = "thinkwork-${var.stage} docs → CloudFront"
229
+ }
230
+
231
+ ################################################################################
232
+ # admin.<domain> → admin CloudFront distribution (optional)
233
+ ################################################################################
234
+
235
+ resource "cloudflare_record" "admin" {
236
+ count = local.create_admin_record ? 1 : 0
237
+
238
+ zone_id = var.cloudflare_zone_id
239
+ name = local.admin
240
+ content = var.admin_cloudfront_domain_name
241
+ type = "CNAME"
242
+ ttl = 300
243
+ proxied = false
244
+ comment = "thinkwork-${var.stage} admin → CloudFront"
245
+ }
@@ -0,0 +1,14 @@
1
+ output "certificate_arn" {
2
+ description = "ACM certificate ARN (us-east-1) covering apex + www. Pass this to the static-site module via www_certificate_arn."
3
+ value = aws_acm_certificate_validation.www.certificate_arn
4
+ }
5
+
6
+ output "www_redirect_distribution_id" {
7
+ description = "CloudFront distribution ID for the www→apex redirect"
8
+ value = aws_cloudfront_distribution.www_redirect.id
9
+ }
10
+
11
+ output "www_redirect_distribution_domain" {
12
+ description = "CloudFront distribution domain for the www→apex redirect"
13
+ value = aws_cloudfront_distribution.www_redirect.domain_name
14
+ }
@@ -0,0 +1,43 @@
1
+ variable "stage" {
2
+ description = "Deployment stage (used for resource naming)"
3
+ type = string
4
+ }
5
+
6
+ variable "domain" {
7
+ description = "Apex domain served by the public website (e.g. thinkwork.ai)"
8
+ type = string
9
+ }
10
+
11
+ variable "cloudflare_zone_id" {
12
+ description = "Cloudflare zone ID for the apex domain. Non-secret; lives in tfvars."
13
+ type = string
14
+ }
15
+
16
+ variable "cloudfront_domain_name" {
17
+ description = "CloudFront distribution domain name for the primary www site (e.g. d123.cloudfront.net). Passed in from the static-site module output."
18
+ type = string
19
+ }
20
+
21
+ variable "include_docs" {
22
+ description = "When true, add docs.<domain> to the ACM cert SANs and create a Cloudflare CNAME for it. Separated from docs_cloudfront_domain_name to avoid a Terraform dependency cycle (distribution depends on cert, so the cert can't depend on the distribution output)."
23
+ type = bool
24
+ default = false
25
+ }
26
+
27
+ variable "docs_cloudfront_domain_name" {
28
+ description = "CloudFront distribution domain name for the docs site. Used as the target for the docs.<domain> Cloudflare CNAME when include_docs is true."
29
+ type = string
30
+ default = ""
31
+ }
32
+
33
+ variable "include_admin" {
34
+ description = "When true, add admin.<domain> to the ACM cert SANs and create a Cloudflare CNAME for it. Same cycle-avoidance rationale as include_docs."
35
+ type = bool
36
+ default = false
37
+ }
38
+
39
+ variable "admin_cloudfront_domain_name" {
40
+ description = "CloudFront distribution domain name for the admin SPA. Used as the target for the admin.<domain> Cloudflare CNAME when include_admin is true."
41
+ type = string
42
+ default = ""
43
+ }
@@ -13,10 +13,22 @@ locals {
13
13
  bucket_name = var.bucket_name != "" ? var.bucket_name : "thinkwork-${var.stage}-storage"
14
14
 
15
15
  # Hindsight is an optional add-on. Preferred toggle: var.enable_hindsight.
16
- # For one release we also honor the deprecated var.memory_engine == "hindsight"
17
- # so existing tfvars keep working. The CLI config command also auto-translates
18
- # between the two. Remove the legacy branch in a future release.
16
+ # For one release we also honor the legacy var.memory_engine == "hindsight"
17
+ # so existing tfvars keep working.
19
18
  hindsight_enabled = var.enable_hindsight || var.memory_engine == "hindsight"
19
+
20
+ # Canonical long-term memory engine for this deployment. Exactly one engine
21
+ # is active per deployment for recall/inspect/export. Auto-selects from
22
+ # enable_hindsight when var.memory_engine is left empty so existing deploys
23
+ # keep working without config changes. Legacy value "managed" maps to
24
+ # "agentcore".
25
+ resolved_memory_engine = (
26
+ var.memory_engine == "hindsight" || var.memory_engine == "agentcore"
27
+ ? var.memory_engine
28
+ : var.memory_engine == "managed"
29
+ ? "agentcore"
30
+ : local.hindsight_enabled ? "hindsight" : "agentcore"
31
+ )
20
32
  }
21
33
 
22
34
  ################################################################################
@@ -66,11 +78,13 @@ module "cognito" {
66
78
 
67
79
  admin_callback_urls = concat(
68
80
  var.admin_callback_urls,
69
- ["https://${module.admin_site.distribution_domain}", "https://${module.admin_site.distribution_domain}/auth/callback"]
81
+ ["https://${module.admin_site.distribution_domain}", "https://${module.admin_site.distribution_domain}/auth/callback"],
82
+ var.admin_domain != "" ? ["https://${var.admin_domain}", "https://${var.admin_domain}/auth/callback"] : []
70
83
  )
71
84
  admin_logout_urls = concat(
72
85
  var.admin_logout_urls,
73
- ["https://${module.admin_site.distribution_domain}"]
86
+ ["https://${module.admin_site.distribution_domain}"],
87
+ var.admin_domain != "" ? ["https://${var.admin_domain}"] : []
74
88
  )
75
89
  mobile_callback_urls = var.mobile_callback_urls
76
90
  mobile_logout_urls = var.mobile_logout_urls
@@ -174,10 +188,13 @@ module "api" {
174
188
  agentcore_function_arn = module.agentcore.agentcore_function_arn
175
189
  hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
176
190
  agentcore_memory_id = module.agentcore_memory.memory_id
191
+ memory_engine = local.resolved_memory_engine
177
192
  admin_url = "https://${module.admin_site.distribution_domain}"
178
193
  docs_url = "https://${module.docs_site.distribution_domain}"
179
194
  appsync_realtime_url = module.appsync.graphql_realtime_url
180
195
  ecr_repository_url = module.agentcore.ecr_repository_url
196
+ job_scheduler_role_arn = module.job_triggers.job_scheduler_role_arn
197
+ lastmile_tasks_api_url = var.lastmile_tasks_api_url
181
198
  }
182
199
 
183
200
  ################################################################################
@@ -206,6 +223,7 @@ module "agentcore" {
206
223
 
207
224
  hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
208
225
  agentcore_memory_id = module.agentcore_memory.memory_id
226
+ memory_engine = local.resolved_memory_engine
209
227
  }
210
228
 
211
229
  module "crons" {
@@ -239,8 +257,16 @@ module "hindsight" {
239
257
  module "ses" {
240
258
  source = "../app/ses-email"
241
259
 
242
- stage = var.stage
243
- account_id = var.account_id
260
+ stage = var.stage
261
+ account_id = var.account_id
262
+ region = var.region
263
+ email_domain = var.ses_inbound_domain
264
+
265
+ inbound_bucket_name = module.s3.bucket_name
266
+ email_inbound_fn_arn = module.api.email_inbound_fn_arn
267
+ email_inbound_fn_name = module.api.email_inbound_fn_name
268
+
269
+ manage_active_rule_set = var.ses_manage_active_rule_set
244
270
  }
245
271
 
246
272
  ################################################################################
@@ -250,8 +276,11 @@ module "ses" {
250
276
  module "admin_site" {
251
277
  source = "../app/static-site"
252
278
 
253
- stage = var.stage
254
- site_name = "admin"
279
+ stage = var.stage
280
+ site_name = "admin"
281
+ is_spa = true
282
+ custom_domain = var.admin_domain
283
+ certificate_arn = var.admin_certificate_arn
255
284
  }
256
285
 
257
286
  ################################################################################
@@ -266,3 +295,17 @@ module "docs_site" {
266
295
  custom_domain = var.docs_domain
267
296
  certificate_arn = var.docs_certificate_arn
268
297
  }
298
+
299
+ ################################################################################
300
+ # Public Website (www)
301
+ ################################################################################
302
+
303
+ module "www_site" {
304
+ source = "../app/static-site"
305
+
306
+ stage = var.stage
307
+ site_name = "www"
308
+ custom_domain = var.www_domain
309
+ certificate_arn = var.www_certificate_arn
310
+ # is_spa defaults to false — SSG output, directory URIs get rewritten to index.html
311
+ }
@@ -127,3 +127,35 @@ output "docs_bucket_name" {
127
127
  description = "S3 bucket for docs site assets"
128
128
  value = module.docs_site.bucket_name
129
129
  }
130
+
131
+ # Public website (www)
132
+ output "www_distribution_id" {
133
+ description = "CloudFront distribution ID for the public website"
134
+ value = module.www_site.distribution_id
135
+ }
136
+
137
+ output "www_distribution_domain" {
138
+ description = "CloudFront domain for the public website"
139
+ value = module.www_site.distribution_domain
140
+ }
141
+
142
+ output "www_bucket_name" {
143
+ description = "S3 bucket for the public website assets"
144
+ value = module.www_site.bucket_name
145
+ }
146
+
147
+ # SES inbound email
148
+ output "ses_inbound_zone_id" {
149
+ description = "Route53 hosted zone ID for the email subdomain (null when ses_inbound_domain is not set)"
150
+ value = module.ses.zone_id
151
+ }
152
+
153
+ output "ses_inbound_name_servers" {
154
+ description = "Name servers for the delegated email subzone. Paste these as NS records at the registrar that hosts the parent domain (e.g. Google Domains) before SES can verify."
155
+ value = module.ses.name_servers
156
+ }
157
+
158
+ output "ses_inbound_mx_target" {
159
+ description = "MX target host for the email subdomain. Terraform already writes this into the subzone — this output is informational."
160
+ value = module.ses.mx_target
161
+ }
@@ -145,13 +145,13 @@ variable "enable_hindsight" {
145
145
  }
146
146
 
147
147
  variable "memory_engine" {
148
- description = "DEPRECATED. Use `enable_hindsight` instead. For backwards compatibility, setting this to 'hindsight' is equivalent to `enable_hindsight = true`. AgentCore managed memory is always on and cannot be disabled."
148
+ description = "Active long-term memory engine for canonical recall/inspect/export. Exactly one engine is authoritative per deployment. Accepted values: 'hindsight' (requires enable_hindsight = true), 'agentcore' (uses the always-on AgentCore managed memory). Legacy value 'managed' maps to 'agentcore'. Empty = auto-select: 'hindsight' when enable_hindsight = true, otherwise 'agentcore'."
149
149
  type = string
150
150
  default = ""
151
151
 
152
152
  validation {
153
- condition = var.memory_engine == "" || contains(["managed", "hindsight"], var.memory_engine)
154
- error_message = "memory_engine (deprecated) must be empty, 'managed', or 'hindsight'. Prefer enable_hindsight instead."
153
+ condition = var.memory_engine == "" || contains(["managed", "hindsight", "agentcore"], var.memory_engine)
154
+ error_message = "memory_engine must be empty, 'hindsight', 'agentcore', or legacy 'managed'."
155
155
  }
156
156
  }
157
157
 
@@ -261,3 +261,57 @@ variable "docs_certificate_arn" {
261
261
  type = string
262
262
  default = ""
263
263
  }
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Public website (custom domain — optional)
267
+ # ---------------------------------------------------------------------------
268
+
269
+ variable "www_domain" {
270
+ description = "Custom domain for the public website (e.g. thinkwork.ai). Leave empty for CloudFront default."
271
+ type = string
272
+ default = ""
273
+ }
274
+
275
+ variable "www_certificate_arn" {
276
+ description = "ACM certificate ARN for the www domain (us-east-1, required for CloudFront custom domains). Covers both the apex and www subdomain."
277
+ type = string
278
+ default = ""
279
+ }
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # Admin site (custom domain — optional)
283
+ # ---------------------------------------------------------------------------
284
+
285
+ variable "admin_domain" {
286
+ description = "Custom domain for the admin SPA (e.g. admin.thinkwork.ai). Leave empty for CloudFront default."
287
+ type = string
288
+ default = ""
289
+ }
290
+
291
+ variable "admin_certificate_arn" {
292
+ description = "ACM certificate ARN for the admin domain (us-east-1, required for CloudFront custom domains)."
293
+ type = string
294
+ default = ""
295
+ }
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # SES inbound email (delegated subzone — Option A)
299
+ # ---------------------------------------------------------------------------
300
+
301
+ variable "ses_inbound_domain" {
302
+ description = "Subdomain used for agent email (e.g. agents.thinkwork.ai). Terraform creates a delegated Route53 hosted zone for this name, manages the SES domain identity + DKIM CNAMEs + MX in that zone, and wires an SES receipt rule that stores inbound mail in S3 and invokes the email-inbound Lambda. Leave empty to skip all SES inbound resources. After first apply, paste the `ses_inbound_name_servers` output as NS records at whatever hosts the parent domain."
303
+ type = string
304
+ default = ""
305
+ }
306
+
307
+ variable "ses_manage_active_rule_set" {
308
+ description = "Activate the SES receipt rule set. Only ONE rule set can be active per region per AWS account; set false on secondary stages that share an account so they don't fight over activation."
309
+ type = bool
310
+ default = true
311
+ }
312
+
313
+ variable "lastmile_tasks_api_url" {
314
+ description = "Base URL of the LastMile Tasks REST API (e.g. https://api-dev.lastmile-tei.com for develop). Feature-flags the outbound task sync — leave blank to keep mobile-created tasks in sync_status='local'."
315
+ type = string
316
+ default = ""
317
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",