unoverse 0.1.125 → 0.1.127

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.
@@ -271,6 +271,28 @@ resource "aws_instance" "app" {
271
271
  volume_type = "gp3"
272
272
  }
273
273
 
274
+ # IMDSv2 REQUIRED, and this is not optional in a governed account.
275
+ #
276
+ # AWS's own recommended guardrail denies ec2:RunInstances unless the launch enforces
277
+ # IMDSv2, and organisations apply it as a Service Control Policy. Without this block the
278
+ # AWS default is `optional`, the condition is not met, and the deny fires:
279
+ #
280
+ # UnauthorizedOperation ... ec2:RunInstances ... with an explicit deny in a service
281
+ # control policy
282
+ #
283
+ # That error names no condition, so it reads as "EC2 is banned here" and sends you to your
284
+ # cloud team for a policy change you do not need. The condition is visible only by
285
+ # decoding the failure message (`aws sts decode-authorization-message`), which showed
286
+ # `ec2:MetadataHttpTokens = required`. Proved by dry run 2026-08-06: identical launch
287
+ # refused without this block, accepted with it.
288
+ #
289
+ # It is the right default everywhere regardless. IMDSv1 lets any process that can forge a
290
+ # simple GET read the instance's credentials; IMDSv2 requires a signed token first.
291
+ metadata_options {
292
+ http_endpoint = "enabled"
293
+ http_tokens = "required"
294
+ }
295
+
274
296
  tags = { Name = "${var.name}-app" }
275
297
  }
276
298
 
@@ -589,10 +611,25 @@ resource "aws_iam_user_policy" "bedrock" {
589
611
  Version = "2012-10-17"
590
612
  Statement = [{
591
613
  Effect = "Allow"
614
+ # WIDE ON RUNTIME, silent on spending.
615
+ #
616
+ # Three named Invoke actions dated fast. `Converse` is the current unified API and the
617
+ # Bedrock nodes already call it; `ApplyGuardrail` is a separate action the guardrail
618
+ # node needs; knowledge bases add `Retrieve`. Each arrival meant editing this list,
619
+ # re-applying, and in a managed account asking an administrator again.
620
+ #
621
+ # NOT `bedrock:*`. That includes CreateProvisionedModelThroughput, which commits money.
622
+ # Everything below is invoke-and-read: it cannot buy capacity, and it cannot create or
623
+ # delete a model or a guardrail.
624
+ #
625
+ # Model access is still granted per model in the Bedrock console. No policy grants it.
592
626
  Action = [
593
- "bedrock:InvokeModel",
594
- "bedrock:InvokeModelWithResponseStream",
595
- "bedrock:InvokeModelWithBidirectionalStream",
627
+ "bedrock:InvokeModel*",
628
+ "bedrock:Converse*",
629
+ "bedrock:ApplyGuardrail",
630
+ "bedrock:Retrieve*",
631
+ "bedrock:List*",
632
+ "bedrock:Get*",
596
633
  ]
597
634
  Resource = "*"
598
635
  }]
@@ -18,6 +18,84 @@
18
18
  # It never touches the developer's terraform.tfvars, their .env, or their code. Only the
19
19
  # cloud resources this ground owns.
20
20
 
21
+ # ── Resources a previous run created and did not write down ───────────────────────────────
22
+ #
23
+ # `destroy` removes what STATE records. A deploy killed mid-apply leaves AWS holding
24
+ # resources terraform never recorded, and they do not merely linger: a database or a load
25
+ # balancer keeps a security group alive, so the part terraform CAN do fails on
26
+ #
27
+ # DependencyViolation: resource sg-... has a dependent object
28
+ #
29
+ # after fifteen minutes of retrying, and the teardown reports only that it did not finish.
30
+ # The operator is then left deleting a database, a Redis cluster and a load balancer by hand
31
+ # before a "one command" teardown will complete. That happened on 2026-08-05.
32
+ #
33
+ # IT IMPORTS. IT NEVER DELETES.
34
+ #
35
+ # Adopting an orphan into state is a state write and touches no cloud API. The deletion is
36
+ # still terraform's, from the plan the operator sees and confirms by name. That matters twice
37
+ # over: terraform already knows the order to take this stack apart (load balancer before its
38
+ # security group, database before the group holding its interface), and a second deletion
39
+ # engine written in bash would not, and would drift from main.tf the day a resource was
40
+ # added.
41
+ #
42
+ # NOTHING WE DID NOT CREATE, enforced four ways rather than trusted:
43
+ #
44
+ # 1. TAGGED AS OURS. `_cloud_orphans` lists only what carries `Universe=<name>`, and
45
+ # `default_tags` stamps that on resources THIS configuration creates. A borrowed
46
+ # database keeps its own tags and can never appear here (main.tf, "Borrowed resources
47
+ # are never owned").
48
+ # 2. A KNOWN ADDRESS. The ARN must map to one of the handful of resources main.tf declares.
49
+ # Anything else is left alone, whatever it is tagged.
50
+ # 3. NAMED AFTER THIS UNIVERSE. The ARN must contain the universe's own name, which is why
51
+ # `aws_instance.app` is deliberately absent below: an EC2 ARN carries only an opaque
52
+ # instance id, so it cannot be verified this way, and an unverifiable match is not one
53
+ # worth making. An untracked server is reported instead.
54
+ # 4. NOT ALREADY TRACKED. An address present in state is skipped, so this can only ever add
55
+ # what is missing, never re-point something the operator already owns.
56
+ #
57
+ # Failure is not fatal anywhere in here: the worst case is the teardown behaving exactly as
58
+ # it did before.
59
+ _adopt_orphans() {
60
+ local dir="$1" uname_="$2" orphans arn addr id adopted=0 unclaimed=""
61
+
62
+ command -v aws >/dev/null 2>&1 || return 0
63
+ orphans=$(_cloud_orphans aws "$uname_" 2>/dev/null)
64
+ [ -n "$orphans" ] && [ "$orphans" != "unknown" ] || return 0
65
+
66
+ while IFS= read -r arn; do
67
+ [ -n "$arn" ] || continue
68
+ addr=""; id="$arn"
69
+ case "$arn" in
70
+ *:rds:*:db:"$uname_"-pg) addr="aws_db_instance.postgres"; id="$uname_-pg" ;;
71
+ *:elasticache:*:replicationgroup:"$uname_"-redis) addr="aws_elasticache_replication_group.redis"; id="$uname_-redis" ;;
72
+ *:loadbalancer/app/"$uname_"-alb/*) addr="aws_lb.public" ;;
73
+ *:targetgroup/"$uname_"-app/*) addr="aws_lb_target_group.app" ;;
74
+ *:targetgroup/"$uname_"-canvas/*) addr="aws_lb_target_group.canvas[0]" ;;
75
+ *) unclaimed="$unclaimed $arn"$'\n' ; continue ;;
76
+ esac
77
+ terraform -chdir="$dir" state list 2>/dev/null | grep -qxF "$addr" && continue
78
+ if terraform -chdir="$dir" import -input=false "$addr" "$id" >/dev/null 2>&1; then
79
+ adopted=$((adopted + 1))
80
+ else
81
+ unclaimed="$unclaimed $arn"$'\n'
82
+ fi
83
+ done <<EOF
84
+ $orphans
85
+ EOF
86
+
87
+ [ "$adopted" -gt 0 ] && \
88
+ info "Adopted $adopted resource(s) an interrupted run left untracked. They are in the plan below"
89
+ # SAID OUT LOUD, never silently swept. Anything here is billing and this teardown will not
90
+ # remove it, which the operator has to know BEFORE they are told the universe is gone.
91
+ if [ -n "$unclaimed" ]; then
92
+ warn "these carry this universe's tag but are not resources this teardown can remove:"
93
+ printf '%s' "$unclaimed"
94
+ echo " Check them yourself before assuming the bill has stopped."
95
+ fi
96
+ return 0
97
+ }
98
+
21
99
  cmd_destroy() {
22
100
  # NAME THE GROUND. This took the first configured one, and digitalocean is first in the
23
101
  # list — so an operator with both grounds who typed `unoverse destroy` meaning AWS would
@@ -46,9 +124,19 @@ cmd_destroy() {
46
124
  echo -e " ${RED}${BOLD}⬡ Taking down your $pretty universe${NC}"
47
125
  echo ""
48
126
 
49
- local tmp planfile rc
127
+ local tmp planfile rc name
50
128
  tmp=$(mktemp -d); planfile="$tmp/plan"
51
129
 
130
+ # The universe's name, read once. It is the tag every resource carries, so it is what
131
+ # decides which untracked resources are this universe's (_adopt_orphans) as well as the
132
+ # word the operator types to confirm.
133
+ name=$(grep -E '^name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
134
+ name="${name:-universe}"
135
+
136
+ # BEFORE THE PLAN, so what an interrupted run left behind is in the list the operator
137
+ # confirms, rather than something they discover by hand an hour later.
138
+ [ "$cloud" = "aws" ] && _adopt_orphans "$dir" "$name"
139
+
52
140
  # APPLY THE CONFIGURATION TO THE DATABASE FIRST, because destroy does not.
53
141
  #
54
142
  # `terraform destroy` deletes a resource as the last apply left it, so an attribute that
@@ -98,10 +186,6 @@ cmd_destroy() {
98
186
  info "Staying: ${BOLD}your own database${NC} ${DIM}(byo_postgres_url — this stack never owned it)${NC}"
99
187
  fi
100
188
 
101
- local name
102
- name=$(grep -E '^name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
103
- name="${name:-universe}"
104
-
105
189
  echo ""
106
190
  # Say what is actually lost. With a reused cluster the DATABASE survives, and claiming
107
191
  # otherwise makes the warning untrue in the commonest case — which teaches people to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.125",
3
+ "version": "0.1.127",
4
4
  "description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",