thinkwork-cli 0.9.0 → 0.9.1

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 (57) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +1187 -315
  4. package/dist/terraform/examples/greenfield/main.tf +325 -19
  5. package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
  6. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
  7. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
  8. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
  9. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
  10. package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
  11. package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
  12. package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
  13. package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
  14. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
  15. package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
  16. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
  17. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
  18. package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
  19. package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
  20. package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
  21. package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
  22. package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
  23. package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
  24. package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
  25. package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
  26. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
  27. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
  28. package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
  29. package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
  30. package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
  31. package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
  32. package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
  33. package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
  34. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
  35. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
  36. package/dist/terraform/modules/app/static-site/main.tf +146 -5
  37. package/dist/terraform/modules/app/www-dns/main.tf +118 -15
  38. package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
  39. package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
  40. package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
  41. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
  42. package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
  43. package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
  44. package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
  45. package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
  46. package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
  47. package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
  48. package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
  49. package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
  50. package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
  51. package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
  52. package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
  53. package/dist/terraform/modules/thinkwork/main.tf +439 -21
  54. package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
  55. package/dist/terraform/modules/thinkwork/variables.tf +153 -2
  56. package/dist/terraform/schema.graphql +17 -0
  57. package/package.json +15 -14
@@ -10,9 +10,9 @@
10
10
  ################################################################################
11
11
 
12
12
  locals {
13
- create = var.create_database
14
- use_aurora = local.create && var.database_engine == "aurora-serverless"
15
- use_rds = local.create && var.database_engine == "rds-postgres"
13
+ create = var.create_database
14
+ use_aurora = local.create && var.database_engine == "aurora-serverless"
15
+ use_rds = local.create && var.database_engine == "rds-postgres"
16
16
 
17
17
  cluster_identifier = "thinkwork-${var.stage}-db"
18
18
  master_username = "thinkwork_admin"
@@ -29,6 +29,16 @@ locals {
29
29
  local.use_rds ? aws_db_instance.main[0].address : var.existing_db_endpoint
30
30
  )
31
31
  db_security_group_id = local.create ? aws_security_group.db[0].id : var.existing_db_security_group_id
32
+
33
+ # aws_s3 Aurora extension opts in when an Aurora cluster exists AND the
34
+ # caller set enable_aws_s3 = true. Plan-time gate is the explicit bool,
35
+ # not the backups_bucket_arn nullness — a freshly-created bucket's ARN
36
+ # is "known after apply," which broke count evaluation on greenfield
37
+ # deploys (see PR #526 for the incident). The ARN is still used inside
38
+ # the IAM policy body (jsonencode interpolates at apply time, fine).
39
+ # Non-aurora engines short-circuit before aws_rds_cluster.main[0] is
40
+ # dereferenced.
41
+ enable_aws_s3 = local.use_aurora && var.enable_aws_s3
32
42
  }
33
43
 
34
44
  data "aws_region" "current" {}
@@ -161,6 +171,97 @@ resource "aws_db_instance" "main" {
161
171
  }
162
172
  }
163
173
 
174
+ ################################################################################
175
+ # aws_s3 extension IAM role (Aurora only; opt-in via backups_bucket_arn)
176
+ #
177
+ # Aurora Postgres ships with an `aws_s3` extension (CREATE EXTENSION IF NOT
178
+ # EXISTS aws_s3 CASCADE) that allows `aws_s3.query_export_to_s3(...)` to
179
+ # write query results directly to S3. It requires an IAM role associated
180
+ # with the cluster + trust for `rds.amazonaws.com` + `s3:PutObject`
181
+ # permission on the target bucket.
182
+ #
183
+ # When `backups_bucket_arn` is set, this block provisions the role, the
184
+ # policy, and the cluster association so that U5 of the thread-detail
185
+ # cleanup plan (packages/database-pg/drizzle/0027_thread_cleanup_drops.sql)
186
+ # can `SELECT aws_s3.query_export_to_s3(...)` without embedding any AWS
187
+ # credentials in the hand-rolled SQL.
188
+ #
189
+ # Post-deploy one-shot step (documented in the plan, not automated here):
190
+ # psql "$DATABASE_URL" -c "CREATE EXTENSION IF NOT EXISTS aws_s3 CASCADE"
191
+ ################################################################################
192
+
193
+ resource "aws_iam_role" "aurora_aws_s3" {
194
+ count = local.enable_aws_s3 ? 1 : 0
195
+
196
+ name = "thinkwork-${var.stage}-aurora-aws-s3"
197
+
198
+ assume_role_policy = jsonencode({
199
+ Version = "2012-10-17"
200
+ Statement = [
201
+ {
202
+ Effect = "Allow"
203
+ Principal = { Service = "rds.amazonaws.com" }
204
+ Action = "sts:AssumeRole"
205
+ },
206
+ ]
207
+ })
208
+
209
+ tags = {
210
+ Name = "thinkwork-${var.stage}-aurora-aws-s3"
211
+ Purpose = "aurora-aws_s3-extension"
212
+ }
213
+ }
214
+
215
+ resource "aws_iam_role_policy" "aurora_aws_s3" {
216
+ count = local.enable_aws_s3 ? 1 : 0
217
+
218
+ name = "thinkwork-${var.stage}-aurora-aws-s3"
219
+ role = aws_iam_role.aurora_aws_s3[0].id
220
+
221
+ policy = jsonencode({
222
+ Version = "2012-10-17"
223
+ Statement = [
224
+ {
225
+ Sid = "PutBackupObjects"
226
+ Effect = "Allow"
227
+ Action = [
228
+ "s3:PutObject",
229
+ "s3:AbortMultipartUpload",
230
+ # GetBucketLocation is required by aws_s3.query_export_to_s3 for the
231
+ # region-matching check; without it exports fail at runtime with an
232
+ # opaque permission error. AWS Aurora docs:
233
+ # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/postgresql-s3-export.html
234
+ "s3:GetBucketLocation",
235
+ ]
236
+ # Object-level actions are scoped to pre-drop/* so this role can only
237
+ # write snapshots produced by destructive migrations, not read or
238
+ # overwrite arbitrary bucket content. GetBucketLocation applies to
239
+ # the bucket ARN itself (it is a bucket-level, not object-level,
240
+ # operation).
241
+ Resource = [
242
+ var.backups_bucket_arn,
243
+ "${var.backups_bucket_arn}/pre-drop/*",
244
+ ]
245
+ },
246
+ ]
247
+ })
248
+ }
249
+
250
+ resource "aws_rds_cluster_role_association" "aurora_aws_s3" {
251
+ count = local.enable_aws_s3 ? 1 : 0
252
+
253
+ db_cluster_identifier = aws_rds_cluster.main[0].id
254
+ feature_name = "s3Export"
255
+ role_arn = aws_iam_role.aurora_aws_s3[0].arn
256
+
257
+ # Force the inline policy to land before RDS validates the role. Terraform's
258
+ # implicit dependency graph only links this resource to the IAM role itself
259
+ # (via role_arn). RDS AddRoleToDBCluster verifies the trust policy + any
260
+ # attached inline policies server-side; applying the association before the
261
+ # policy has propagated can return AccessDenied on the first apply.
262
+ depends_on = [aws_iam_role_policy.aurora_aws_s3]
263
+ }
264
+
164
265
  ################################################################################
165
266
  # Secrets Manager — DB Credentials (shared by both engines)
166
267
  ################################################################################
@@ -183,3 +284,63 @@ resource "aws_secretsmanager_secret_version" "db_credentials" {
183
284
  password = var.db_password
184
285
  })
185
286
  }
287
+
288
+ ################################################################################
289
+ # Secrets Manager — Compliance Role Credentials (Phase 3 U2)
290
+ #
291
+ # Three role-scoped secret containers for the compliance.* schema introduced
292
+ # in U1 (drizzle/0069_compliance_schema.sql, PR #880). Per Decision #4 of the
293
+ # master plan (docs/plans/2026-05-06-011-feat-compliance-audit-event-log-plan.md):
294
+ #
295
+ # - compliance/writer-credentials: used by Yoga resolvers + Lambda handlers
296
+ # via the U3 emitAuditEvent helper.
297
+ # - compliance/drainer-credentials: used by the U4 outbox drainer Lambda
298
+ # (reserved-concurrency=1).
299
+ # - compliance/reader-credentials: used by the graphql-http Lambda for
300
+ # U10 admin Compliance read paths.
301
+ #
302
+ # Naming follows the slash-delimited "thinkwork/${stage}/..." convention from
303
+ # CLAUDE.md (the master `db_credentials` secret above uses the grandfathered
304
+ # hyphen form). JSON shape is enriched vs the master's {username, password}:
305
+ # {username, password, host, port, dbname} so each consumer is self-contained.
306
+ #
307
+ # Greenfield bootstrap is operator-driven via scripts/bootstrap-compliance-roles.sh
308
+ # which reads passwords from env, populates these secrets via
309
+ # `aws secretsmanager put-secret-value`, and runs
310
+ # drizzle/0070_compliance_aurora_roles.sql to create the matching Aurora roles.
311
+ # Terraform owns the SECRET CONTAINER; the operator owns the SECRET VALUE.
312
+ #
313
+ # `lifecycle.ignore_changes = [secret_string]` lets operators rotate via the
314
+ # bootstrap script (or AWS console) without Terraform clobbering the value
315
+ # on the next apply.
316
+ ################################################################################
317
+
318
+ resource "aws_secretsmanager_secret" "compliance_writer" {
319
+ count = local.create ? 1 : 0
320
+ name = "thinkwork/${var.stage}/compliance/writer-credentials"
321
+
322
+ tags = {
323
+ Name = "thinkwork-${var.stage}-compliance-writer-credentials"
324
+ Role = "compliance_writer"
325
+ }
326
+ }
327
+
328
+ resource "aws_secretsmanager_secret" "compliance_drainer" {
329
+ count = local.create ? 1 : 0
330
+ name = "thinkwork/${var.stage}/compliance/drainer-credentials"
331
+
332
+ tags = {
333
+ Name = "thinkwork-${var.stage}-compliance-drainer-credentials"
334
+ Role = "compliance_drainer"
335
+ }
336
+ }
337
+
338
+ resource "aws_secretsmanager_secret" "compliance_reader" {
339
+ count = local.create ? 1 : 0
340
+ name = "thinkwork/${var.stage}/compliance/reader-credentials"
341
+
342
+ tags = {
343
+ Name = "thinkwork-${var.stage}-compliance-reader-credentials"
344
+ Role = "compliance_reader"
345
+ }
346
+ }
@@ -28,3 +28,37 @@ output "database_engine" {
28
28
  description = "Which engine is running (aurora-serverless or rds-postgres)"
29
29
  value = var.database_engine
30
30
  }
31
+
32
+ output "aws_s3_iam_role_arn" {
33
+ description = "ARN of the IAM role attached to the Aurora cluster for `aws_s3.query_export_to_s3` (only when backups_bucket_arn is set). Null otherwise. Useful for confirming the role attachment in post-deploy runbooks."
34
+ value = local.enable_aws_s3 ? aws_iam_role.aurora_aws_s3[0].arn : null
35
+ }
36
+
37
+ # ----------------------------------------------------------------------------
38
+ # Compliance role secret ARNs (Phase 3 U2)
39
+ #
40
+ # Container-only — Terraform creates the AWS Secrets Manager resource;
41
+ # secret values are populated by scripts/bootstrap-compliance-roles.sh
42
+ # alongside the matching Aurora roles in
43
+ # drizzle/0070_compliance_aurora_roles.sql.
44
+ #
45
+ # Consumers:
46
+ # compliance_writer_secret_arn → U3 emitAuditEvent helper (resolver path)
47
+ # compliance_drainer_secret_arn → U4 outbox drainer Lambda
48
+ # compliance_reader_secret_arn → U10 graphql-http Compliance read path
49
+ # ----------------------------------------------------------------------------
50
+
51
+ output "compliance_writer_secret_arn" {
52
+ description = "Secrets Manager ARN for the compliance_writer Aurora role (Phase 3 U2). Empty string when create_database = false."
53
+ value = local.create ? aws_secretsmanager_secret.compliance_writer[0].arn : ""
54
+ }
55
+
56
+ output "compliance_drainer_secret_arn" {
57
+ description = "Secrets Manager ARN for the compliance_drainer Aurora role (Phase 3 U2). Empty string when create_database = false."
58
+ value = local.create ? aws_secretsmanager_secret.compliance_drainer[0].arn : ""
59
+ }
60
+
61
+ output "compliance_reader_secret_arn" {
62
+ description = "Secrets Manager ARN for the compliance_reader Aurora role (Phase 3 U2). Empty string when create_database = false."
63
+ value = local.create ? aws_secretsmanager_secret.compliance_reader[0].arn : ""
64
+ }
@@ -112,3 +112,19 @@ variable "deletion_protection" {
112
112
  type = bool
113
113
  default = null
114
114
  }
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # aws_s3 Aurora extension (optional — set backups_bucket_arn to enable)
118
+ # ---------------------------------------------------------------------------
119
+
120
+ variable "backups_bucket_arn" {
121
+ description = "ARN of the S3 backups bucket Aurora should be allowed to write to via the aws_s3 extension (aws_s3.query_export_to_s3). Used as the Resource in the attached IAM policy; only takes effect when enable_aws_s3 = true. A not-yet-created bucket's ARN is 'known after apply' — gating on this nullness broke count at plan time, so use enable_aws_s3 as the plan-time gate instead."
122
+ type = string
123
+ default = null
124
+ }
125
+
126
+ variable "enable_aws_s3" {
127
+ description = "Plan-time gate for the aws_s3 Aurora extension IAM role. Set true when Aurora should be allowed to PutObject into backups_bucket_arn. Only effective when database_engine = 'aurora-serverless'. Defaults to false to preserve the prior 'opt-in by passing backups_bucket_arn' shape when this variable is omitted."
128
+ type = bool
129
+ default = false
130
+ }
@@ -0,0 +1,145 @@
1
+ # compliance-audit-bucket
2
+
3
+ WORM-protected S3 bucket for SOC2 Type 1 tamper-evident audit anchoring. Provisions the bucket that the anchor Lambda (U8a/U8b) writes Merkle-anchor evidence into, and ships the IAM role the Lambda will eventually assume.
4
+
5
+ Module path: `terraform/modules/data/compliance-audit-bucket/` (master-plan canonical name).
6
+ Bucket name: `thinkwork-${var.stage}-compliance-anchors` (master plan line 513 — plural, distinct from the module name).
7
+
8
+ ## What this is
9
+
10
+ - An S3 bucket with Object Lock enabled at create time. Object Lock cannot be disabled after creation; this is a one-way commitment per AWS.
11
+ - Two prefixes:
12
+ - `anchors/` — Merkle-anchor objects. Subject to default retention. Lifecycle transitions to Glacier IR at 90 days. **No** expiration rule.
13
+ - `proofs/` — per-tenant proof slices written by the anchor Lambda. The bucket-level Lock applies, but U8b sets a shorter per-object retention because slices are derivable from the chain + anchor.
14
+ - A bucket policy denying any `s3:*` over plain HTTP and denying `s3:DeleteObject` / `s3:DeleteObjectVersion` from any principal — defense-in-depth on top of Object Lock.
15
+ - An IAM role (`thinkwork-${stage}-compliance-anchor-lambda-role`) that U8a's anchor Lambda will assume. The role's inline policy is path-scoped to `anchors/*` and `proofs/*`, grants only the actions the writer needs, and **explicitly denies** `s3:BypassGovernanceRetention` and `s3:PutObjectLegalHold` so the deny survives any future broadening of the role's IAM grants.
16
+
17
+ ## Object Lock posture
18
+
19
+ | Stage | Default `mode` | Default retention | Notes |
20
+ |-------|----------------|-------------------|-------|
21
+ | dev / staging | `GOVERNANCE` | 365 days | Allows a privileged role with `s3:BypassGovernanceRetention` to delete or shorten retention. Required for dev iteration; the anchor Lambda role itself does **not** hold the bypass action (explicitly denied). |
22
+ | prod (audit-engagement time) | `COMPLIANCE` | 365 days | Irreversible — even AWS root cannot delete or shorten retention until it expires. |
23
+
24
+ ### COMPLIANCE-mode cutover playbook (prod)
25
+
26
+ The flip is a one-line tfvars change in the prod stack at audit-engagement time:
27
+
28
+ ```hcl
29
+ # terraform/examples/greenfield/terraform.tfvars (prod)
30
+ compliance_anchor_object_lock_mode = "COMPLIANCE"
31
+ ```
32
+
33
+ After `terraform apply`, verify with:
34
+
35
+ ```bash
36
+ aws s3api get-object-lock-configuration \
37
+ --bucket thinkwork-prod-compliance-anchors \
38
+ --query 'ObjectLockConfiguration.Rule.DefaultRetention.Mode'
39
+ # Expected: "COMPLIANCE"
40
+ ```
41
+
42
+ Once flipped to COMPLIANCE, the bucket's default retention cannot be shortened by any principal, including AWS root. The flip itself is reversible by Terraform plan only until the first object is written; after that, AWS retains the lock state for the full retention window regardless of subsequent configuration changes.
43
+
44
+ ## `force_destroy = false` invariant
45
+
46
+ `force_destroy` is hardcoded `false`. Object Lock + `force_destroy` interact pathologically:
47
+
48
+ - **COMPLIANCE mode:** `terraform destroy` cannot delete locked objects until retention expires (365 days). `force_destroy = true` would fail with `AccessDenied (403 Forbidden)` on every object.
49
+ - **GOVERNANCE mode:** `force_destroy = true` would only succeed if the deploying principal holds `s3:BypassGovernanceRetention`, which we explicitly do not grant. Granting it would defeat the audit-evidence posture.
50
+
51
+ Either way, `force_destroy = true` masks Object Lock retention behavior. Don't ship it.
52
+
53
+ ### Dev cleanup playbook
54
+
55
+ > **U8b cutover note (2026-05-07):** the anchor Lambda now writes real WORM-locked
56
+ > bytes on every 15-minute cadence. A dev bucket that's been live for any non-trivial
57
+ > period accumulates objects under default 365-day retention — the dev stage's
58
+ > `GOVERNANCE` mode is the *only* thing that makes this playbook achievable. The
59
+ > `proofs/` prefix relies on the bucket-default lock; both anchor and proof objects
60
+ > require the bypass action below to delete. **Do not run this playbook against a
61
+ > COMPLIANCE-mode bucket** (prod) — the `s3:BypassGovernanceRetention` action is
62
+ > ineffective and the only recovery is rotating the bucket name on a fresh stage.
63
+
64
+ Tearing down a dev bucket requires admin-tier intervention (not a routine operator action) and is **not supported by `terraform destroy` alone**. The repo does not currently provision a break-glass role with `s3:BypassGovernanceRetention`; the operator performs the cleanup using their own admin credentials (or grants themselves the bypass action ad-hoc via an IAM policy attachment for the duration of the cleanup, then revokes it).
65
+
66
+ 1. **Pre-requisite**: confirm your active credentials hold both `s3:BypassGovernanceRetention` AND `s3:DeleteObjectVersion` on the bucket. The anchor Lambda role itself **does not** hold these — it is explicitly Denied. If your admin role lacks them, attach a temporary inline policy:
67
+ ```bash
68
+ aws iam put-role-policy --role-name <your-admin-role> \
69
+ --policy-name compliance-anchor-bypass-temp \
70
+ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:BypassGovernanceRetention","s3:DeleteObjectVersion","s3:DeleteObject"],"Resource":"arn:aws:s3:::thinkwork-dev-compliance-anchors/*"}]}'
71
+ ```
72
+ 2. Empty the bucket with the bypass flag (the bucket policy still denies `s3:DeleteObject` from any principal — see "Bucket-policy interaction" below before running this):
73
+ ```bash
74
+ # First, you'll need to remove the DenyDeleteObject statement from the
75
+ # bucket policy temporarily. See Step 3 of "Bucket-policy interaction".
76
+ aws s3api list-object-versions --bucket thinkwork-dev-compliance-anchors \
77
+ --query '{Objects: Versions[].{Key: Key, VersionId: VersionId}}' \
78
+ | aws s3api delete-objects --bucket thinkwork-dev-compliance-anchors \
79
+ --bypass-governance-retention --delete file:///dev/stdin
80
+ # Repeat for delete markers if any (DeleteMarkers in list-object-versions output).
81
+ ```
82
+ 3. Delete the bucket:
83
+ ```bash
84
+ aws s3api delete-bucket --bucket thinkwork-dev-compliance-anchors
85
+ ```
86
+ 4. Wait at least 1 hour before recreating with the same name (S3 bucket-name reuse latency).
87
+ 5. If `terraform apply` is impatient, `terraform state rm module.compliance_anchors.aws_s3_bucket.anchor` to avoid the recreate-failure loop.
88
+ 6. Revoke the temporary policy from step 1: `aws iam delete-role-policy --role-name <your-admin-role> --policy-name compliance-anchor-bypass-temp`.
89
+
90
+ #### Bucket-policy interaction
91
+
92
+ This module's bucket policy includes a `DenyDeleteObject` statement that applies to **all principals**, including admin roles with `s3:BypassGovernanceRetention`. To complete the dev cleanup, you must temporarily replace the bucket policy with one that omits the `DenyDeleteObject` statement (or attaches a Condition exempting your admin role):
93
+
94
+ ```bash
95
+ # 1. Save the current policy
96
+ aws s3api get-bucket-policy --bucket thinkwork-dev-compliance-anchors \
97
+ --query Policy --output text > /tmp/anchor-bucket-policy-backup.json
98
+
99
+ # 2. Replace with a permissive policy (delete-allow only — keep EnforceHTTPS)
100
+ aws s3api put-bucket-policy --bucket thinkwork-dev-compliance-anchors \
101
+ --policy '{"Version":"2012-10-17","Statement":[{"Sid":"EnforceHTTPS","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::thinkwork-dev-compliance-anchors","arn:aws:s3:::thinkwork-dev-compliance-anchors/*"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}'
102
+
103
+ # 3. Now run the empty-bucket commands from the main playbook
104
+ # 4. After delete, the policy is gone with the bucket — no restore needed.
105
+ ```
106
+
107
+ This playbook applies only to GOVERNANCE-mode buckets. COMPLIANCE-mode buckets cannot be emptied until retention expires — by design.
108
+
109
+ ## KMS dependency
110
+
111
+ This module is **the org's first real consumer** of `module.kms.aws_kms_key.main` (the `alias/thinkwork-${stage}` CMK). The default key policy uses the standard "root account" statement, which permits any same-account principal whose IAM policy allows the action — the anchor Lambda role's inline policy fully covers this.
112
+
113
+ If a future PR tightens the KMS key policy without explicitly naming the anchor Lambda role, anchor writes will start failing with `KMSAccessDenied` at runtime — the failure mode is silent (no Terraform plan diff). The variable-level validation and resource-level `lifecycle.precondition` on the SSE configuration catch a missing `kms_key_arn` at plan time, but they cannot catch a key-policy regression. U8b's smoke test exercises the full Put/Get path and is the primary integration check.
114
+
115
+ ## Provider pin
116
+
117
+ This module is compatible with the repo-wide `hashicorp/aws ~> 5.0` pin (locked to `5.100.0` in `terraform/examples/greenfield/.terraform.lock.hcl`). The `aws_s3_bucket_object_lock_configuration` standalone resource (current shape; not the deprecated inline `object_lock_configuration` block on `aws_s3_bucket`) has been stable since v4.0 (Feb 2022 split-resource refactor).
118
+
119
+ ## Inputs
120
+
121
+ | Variable | Type | Default | Description |
122
+ |----------|------|---------|-------------|
123
+ | `stage` | string | _required_ | Deployment stage (e.g., `dev`, `prod`). Stages `prod` and `production` enforce `mode = "COMPLIANCE"` via Terraform `precondition`. |
124
+ | `account_id` | string | _required_ | AWS account ID. Used as `aws:SourceAccount` condition on the anchor Lambda role's trust policy (confused-deputy defense). |
125
+ | `bucket_name` | string | _required_ | Bucket name. Master-plan canonical: `thinkwork-${stage}-compliance-anchors`. |
126
+ | `kms_key_arn` | string | _required_ | CMK ARN for SSE-KMS. Wired from `module.kms.key_arn` at the composite root. Validated non-empty. |
127
+ | `mode` | string | `"GOVERNANCE"` | Object Lock retention mode. Validated ∈ {`GOVERNANCE`, `COMPLIANCE`}. Production stages reject `GOVERNANCE` via plan-time `precondition`. |
128
+ | `retention_days` | number | `365` | Default retention in days. Validated > 0. |
129
+
130
+ ## Outputs
131
+
132
+ | Output | Description |
133
+ |--------|-------------|
134
+ | `bucket_name` | Bucket id (= `var.bucket_name`). |
135
+ | `bucket_arn` | Bucket ARN. |
136
+ | `lambda_role_arn` | IAM role ARN the anchor Lambda will assume (inert in U7 — U8a wires the function). |
137
+
138
+ ## See also
139
+
140
+ - Master plan: `docs/plans/2026-05-06-011-feat-compliance-audit-event-log-plan.md` (U7 entry).
141
+ - U7 sub-plan: `docs/plans/2026-05-07-009-feat-compliance-u7-anchor-bucket-plan.md`.
142
+ - U8a sub-plan: `docs/plans/2026-05-07-010-feat-compliance-u8a-anchor-lambda-inert-plan.md` (inert seam).
143
+ - U8b sub-plan: `docs/plans/2026-05-07-012-feat-compliance-u8b-anchor-lambda-live-plan.md` (live S3 PutObject + Object Lock retention).
144
+ - AWS S3 Object Lock: <https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html>.
145
+ - Cohasset Associates Compliance Assessment: <https://d1.awsstatic.com/r2018/b/S3-Object-Lock/Amazon-S3-Compliance-Assessment.pdf>.