carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

Fleet Architecture Diagram Implementation Plan

> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Give operators one page — /fleet/architecture — that draws how a request reaches a process (DNS → IP → box → app) over a live, provider-neutral inventory of everything the deployment's cloud account has stood up, with each box's size, cost, apps and instance counts.

Architecture: A provider seam (internal/infraview) defines what an inventory is in cloud-neutral terms; internal/awsinv is the only package that imports an AWS SDK and implements it. An hourly console pass writes infra/inventory.json to the deployment bucket. internal/console/archgraph.go joins that snapshot with the fleet aggregate, the per-box snapshots and the registry, lays the result out deterministically, and emits inline SVG. The page is operator-gated with handleFleet's exact 404 shape.

Tech Stack: Go 1.x, aws-sdk-go-v2 (ec2, s3, route53, iam, cloudwatch, acm, resourcegroupstaggingapi — the first four already vendored), html/template, net/http ServeMux, SQLite registry, S3-backed release.Store.

Spec: docs/superpowers/specs/2026-08-19-fleet-architecture-diagram-design.md

Global Constraints

  • Gate before any commit: go build ./..., go vet ./..., go test ./... -count=1 all clean, every package. gofmt -l clean for files you touch (three pre-existing unformatted files elsewhere are left alone).
  • Sandbox env for go commands: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off. A "read-only file system" error from the toolchain means the sandbox needs disabling for that command.
  • Cross-compile matters: CI also runs GOOS=darwin go build ./.... Nothing in these packages may assume Linux.
  • Wire shapes are additive. infra/inventory.json is written to a production bucket. Every field is additive, unknown fields must parse, and an unknown Kind renders as other rather than being dropped.
  • No AWS mutation, ever. No call in internal/awsinv may create, modify, tag or delete anything. The IAM policy in Task 8 contains no write action.
  • Never read or render secret material: no access keys, no IAM policy documents, no bucket contents, no instance user-data, no SSM parameters.
  • Operator gate shape is fixed: session → IsOperator → bare http.NotFound on every failure path including the store error (internal/console/server.go:1720). Never renderMessage for a gate failure — a styled page confirms the route exists.
  • Every change lands through a PR. No local merge to main, no direct commit to main.
  • Commit at the end of every task, message body ending with: Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---

File Structure

| File | Responsibility | |---|---| | internal/infraview/infraview.go | Resource, Inventory, Provider, kind constants, InventoryKey. No cloud SDK. | | internal/infraview/infraview_test.go | Round-trip, additive-parse, unknown-kind tests. | | internal/awsinv/awsinv.go | Provider implementation: assembly, region loop, Partial, auth classification. | | internal/awsinv/clients.go | Narrow per-service interfaces + the real client factory. The only AWS SDK imports. | | internal/awsinv/ec2.go | EC2 → machine/disk/ip/firewall/network mappings. | | internal/awsinv/rest.go | S3, Route53, IAM, CloudWatch, ACM mappings + the tagging catch-all. | | internal/awsinv/*_test.go | Fake clients, one test per mapping, failure-to-Partial tests. | | internal/console/infrapass.go | Hourly pass, CAS write of infra/inventory.json, auth latch, refresh single-flight. | | internal/console/infrapass_test.go | Latch, transient-retry, CAS-write tests. | | internal/console/archgraph.go | Joins + view model (ArchView, BoxCard, ResourceGroup, Graph). | | internal/console/archlayout.go | Layering, ordering, SVG emission. | | internal/console/archgraph_test.go, archlayout_test.go | Join, cost/drift, determinism tests. | | internal/console/archpage.go | handleArchitecture, handleArchitectureRefresh. | | internal/console/archpage_test.go | Gate, content, refresh-door tests. | | internal/console/templates/architecture.html | The page. | | internal/console/server.go | Route registration, page fields, Server seam field. | | internal/console/templates/fleet.html | One link to the new page. | | cmd/console/main.go | Start the pass. | | infra/modules/region-host/main.tf | Read-only inventory IAM grants. |

---

Task 1: internal/infraview — the provider-neutral inventory

Files:

  • Create: internal/infraview/infraview.go
  • Test: internal/infraview/infraview_test.go

Interfaces:

  • Consumes: nothing.
  • Produces: infraview.Resource, infraview.Inventory, infraview.Provider, the Kind* constants, infraview.InventoryKey (= "infra/inventory.json"), Inventory.ByKind(kind string) []Resource, Inventory.Find(kind, attr, value string) *Resource.
  • [ ] Step 1: Write the failing test
package infraview

import (
	"encoding/json"
	"testing"
	"time"
)

// An inventory written before a field existed must still parse, and a kind
// no reader knows must survive as itself rather than being dropped — the
// repo's additive wire-shape rule applied to a bucket object.
func TestInventoryParsesAdditively(t *testing.T) {
	old := []byte(`{
		"provider":"aws","account":"1234","taken_at":"2026-08-19T10:00:00Z",
		"resources":[
			{"provider":"aws","kind":"machine","native_type":"ec2:instance",
			 "id":"i-123","name":"carlos-eu-west-1","region":"eu-west-1",
			 "attrs":{"type":"t4g.medium","public_ip":"52.0.0.1"}},
			{"provider":"aws","kind":"quantum_widget","native_type":"qc:widget","id":"q-1"}
		],
		"future_field":"ignored"
	}`)
	var inv Inventory
	if err := json.Unmarshal(old, &inv); err != nil {
		t.Fatalf("parse: %v", err)
	}
	if inv.Provider != "aws" || inv.Account != "1234" {
		t.Fatalf("header: %+v", inv)
	}
	if !inv.TakenAt.Equal(time.Date(2026, 8, 19, 10, 0, 0, 0, time.UTC)) {
		t.Fatalf("taken_at: %v", inv.TakenAt)
	}
	if len(inv.Resources) != 2 {
		t.Fatalf("resources: %d, want 2 (an unknown kind must not be dropped)", len(inv.Resources))
	}
	if got := inv.Resources[1].Kind; got != "quantum_widget" {
		t.Fatalf("unknown kind = %q, want it carried verbatim", got)
	}
}

func TestByKindAndFind(t *testing.T) {
	inv := Inventory{Resources: []Resource{
		{Kind: KindMachine, ID: "i-1", Attrs: map[string]string{"public_ip": "52.0.0.1"}},
		{Kind: KindMachine, ID: "i-2", Attrs: map[string]string{"public_ip": "52.0.0.2"}},
		{Kind: KindBucket, ID: "carlos-data-ie"},
	}}
	if got := len(inv.ByKind(KindMachine)); got != 2 {
		t.Fatalf("ByKind(machine) = %d, want 2", got)
	}
	if got := inv.Find(KindMachine, "public_ip", "52.0.0.2"); got == nil || got.ID != "i-2" {
		t.Fatalf("Find = %+v, want i-2", got)
	}
	if got := inv.Find(KindMachine, "public_ip", "10.0.0.9"); got != nil {
		t.Fatalf("Find(miss) = %+v, want nil", got)
	}
}

// A resource with no Attrs map must not panic any accessor — an inventory
// read from an older writer is full of them.
func TestFindNilAttrs(t *testing.T) {
	inv := Inventory{Resources: []Resource{{Kind: KindMachine, ID: "i-1"}}}
	if got := inv.Find(KindMachine, "public_ip", "52.0.0.1"); got != nil {
		t.Fatalf("Find over nil Attrs = %+v, want nil", got)
	}
}
  • [ ] Step 2: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/infraview/ -count=1 Expected: FAIL — the package does not compile (undefined: Inventory).

  • [ ] Step 3: Write minimal implementation
// Package infraview is what an infrastructure inventory IS, in terms no
// cloud provider owns. It is deliberately free of every SDK: the AWS
// implementation (internal/awsinv) imports this package, never the other
// way round, so the console page that renders an inventory can be pointed
// at a second provider by writing one file rather than by rewriting the
// page.
//
// The wire shape here is written to a production bucket
// (InventoryKey), so it obeys the repo's additive rule: every field is
// optional, unknown fields parse, and an unknown Kind is carried through
// as itself. A reader that drops what it does not recognise would make the
// page LESS useful exactly where it is most useful — the resource nobody
// wrote code for is the one worth seeing.
package infraview

import (
	"context"
	"time"
)

// InventoryKey is where a deployment's inventory snapshot lives in its own
// bucket. One object, overwritten in place: this is a current-state
// snapshot, not a history, and a history would need a retention policy
// nobody has asked for.
const InventoryKey = "infra/inventory.json"

// The kind vocabulary. Small and neutral on purpose — a provider maps its
// own types into these and carries the truth in NativeType, so the page's
// layout rules ("machines sit in the box layer") never have to learn a new
// cloud's nouns. KindOther is not a failure: it is the honest home for a
// discovered resource this vocabulary has no word for.
const (
	KindMachine     = "machine"
	KindIP          = "ip"
	KindDisk        = "disk"
	KindFirewall    = "firewall"
	KindNetwork     = "network"
	KindBucket      = "bucket"
	KindDNSZone     = "dns_zone"
	KindDNSRecord   = "dns_record"
	KindIdentity    = "identity"
	KindAlarm       = "alarm"
	KindCertificate = "certificate"
	KindOther       = "other"
)

// Resource is one thing a provider is carrying. Attrs is stringly on
// purpose: a typed struct per kind would need a migration every time a
// provider exposes a new field, and this object must stay parseable by
// readers written before that field existed. Keys the console renders are
// documented where it reads them; keys it does not know are shown verbatim.
type Resource struct {
	Provider   string            `json:"provider"`
	Kind       string            `json:"kind"`
	NativeType string            `json:"native_type,omitempty"`
	ID         string            `json:"id"`
	Name       string            `json:"name,omitempty"`
	Region     string            `json:"region,omitempty"`
	Attrs      map[string]string `json:"attrs,omitempty"`
	Tags       map[string]string `json:"tags,omitempty"`
}

// Attr reads one attribute without the caller having to nil-check the map.
func (r Resource) Attr(key string) string {
	if r.Attrs == nil {
		return ""
	}
	return r.Attrs[key]
}

// Inventory is one provider's estate at one moment. Partial names the reads
// that FAILED, in words a person can act on ("security groups in eu-west-1:
// throttled"). It exists because "no security groups" and "the security
// group read failed" are different sentences and only one of them may be
// rendered as a fact.
type Inventory struct {
	Provider  string     `json:"provider"`
	Account   string     `json:"account,omitempty"`
	TakenAt   time.Time  `json:"taken_at"`
	Resources []Resource `json:"resources"`
	Partial   []string   `json:"partial,omitempty"`
}

// ByKind returns every resource of one kind, in inventory order.
func (i Inventory) ByKind(kind string) []Resource {
	var out []Resource
	for _, r := range i.Resources {
		if r.Kind == kind {
			out = append(out, r)
		}
	}
	return out
}

// Find returns the first resource of kind whose attr equals value, or nil.
// value == "" never matches: an empty attribute is absence, and matching on
// it would join every machine with no public IP to every record with no
// value.
func (i Inventory) Find(kind, attr, value string) *Resource {
	if value == "" {
		return nil
	}
	for idx := range i.Resources {
		if i.Resources[idx].Kind == kind && i.Resources[idx].Attr(attr) == value {
			return &i.Resources[idx]
		}
	}
	return nil
}

// Provider is one cloud (or one bare-metal estate) that can describe
// itself. Implementations return construction-class failures as an error —
// no credential, no reachable API — and everything else through
// Inventory.Partial, because a page that can show eleven resource classes
// must show eleven.
type Provider interface {
	Inventory(ctx context.Context) (Inventory, error)
}
  • [ ] Step 4: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/infraview/ -count=1 Expected: PASS (3 tests).

  • [ ] Step 5: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
gofmt -l internal/infraview
git add internal/infraview
git commit -m "infraview: provider-neutral infrastructure inventory shape

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 2: internal/awsinv — client seam and EC2 mappings

Files:

  • Create: internal/awsinv/clients.go, internal/awsinv/ec2.go
  • Test: internal/awsinv/ec2_test.go
  • Modify: go.mod (add service/ec2, service/cloudwatch, service/acm, service/resourcegroupstaggingapi)

Interfaces:

  • Consumes: infraview.Resource, the Kind* constants.
  • Produces: awsinv.Clients struct, awsinv.Factory func type, awsinv.ec2Resources(ctx, EC2API, region string) ([]infraview.Resource, []string) returning resources and failure lines.
  • [ ] Step 1: Add the SDK dependencies
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod go get \
  github.com/aws/aws-sdk-go-v2/service/ec2 \
  github.com/aws/aws-sdk-go-v2/service/cloudwatch \
  github.com/aws/aws-sdk-go-v2/service/acm \
  github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi

(GOPROXY=off must NOT be set for this one step; it is the only step in the plan that fetches.)

  • [ ] Step 2: Write the failing test
package awsinv

import (
	"context"
	"errors"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/carlosframework/platform/internal/infraview"
)

// fakeEC2 answers whatever the test hands it; a non-nil err field for a
// call makes that ONE call fail, which is how the partial-failure rule
// below is proven.
type fakeEC2 struct {
	instances *ec2.DescribeInstancesOutput
	volumes   *ec2.DescribeVolumesOutput
	addresses *ec2.DescribeAddressesOutput
	groups    *ec2.DescribeSecurityGroupsOutput
	vpcs      *ec2.DescribeVpcsOutput
	subnets   *ec2.DescribeSubnetsOutput
	regions   *ec2.DescribeRegionsOutput
	errVols   error
}

func (f *fakeEC2) DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
	return f.instances, nil
}
func (f *fakeEC2) DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) {
	if f.errVols != nil {
		return nil, f.errVols
	}
	return f.volumes, nil
}
func (f *fakeEC2) DescribeAddresses(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) {
	return f.addresses, nil
}
func (f *fakeEC2) DescribeSecurityGroups(context.Context, *ec2.DescribeSecurityGroupsInput, ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) {
	return f.groups, nil
}
func (f *fakeEC2) DescribeVpcs(context.Context, *ec2.DescribeVpcsInput, ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) {
	return f.vpcs, nil
}
func (f *fakeEC2) DescribeSubnets(context.Context, *ec2.DescribeSubnetsInput, ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) {
	return f.subnets, nil
}
func (f *fakeEC2) DescribeRegions(context.Context, *ec2.DescribeRegionsInput, ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) {
	return f.regions, nil
}

func find(rs []infraview.Resource, kind, id string) *infraview.Resource {
	for i := range rs {
		if rs[i].Kind == kind && rs[i].ID == id {
			return &rs[i]
		}
	}
	return nil
}

func TestEC2InstanceMapping(t *testing.T) {
	f := &fakeEC2{instances: &ec2.DescribeInstancesOutput{
		Reservations: []ec2types.Reservation{{Instances: []ec2types.Instance{{
			InstanceId:       aws.String("i-123"),
			InstanceType:     ec2types.InstanceTypeT4gMedium,
			State:            &ec2types.InstanceState{Name: ec2types.InstanceStateNameRunning},
			Placement:        &ec2types.Placement{AvailabilityZone: aws.String("eu-west-1a")},
			PublicIpAddress:  aws.String("52.0.0.1"),
			PrivateIpAddress: aws.String("10.0.0.5"),
			Tags:             []ec2types.Tag{{Key: aws.String("Name"), Value: aws.String("carlos-eu-west-1")}},
		}}}},
	}}

	rs, partial := ec2Resources(context.Background(), f, "eu-west-1")
	if len(partial) != 0 {
		t.Fatalf("partial = %v, want none", partial)
	}
	m := find(rs, infraview.KindMachine, "i-123")
	if m == nil {
		t.Fatalf("no machine in %+v", rs)
	}
	if m.Name != "carlos-eu-west-1" {
		t.Errorf("Name = %q, want the Name tag", m.Name)
	}
	if m.Region != "eu-west-1" || m.NativeType != "ec2:instance" {
		t.Errorf("region/type = %q/%q", m.Region, m.NativeType)
	}
	for k, want := range map[string]string{
		"type": "t4g.medium", "state": "running", "az": "eu-west-1a",
		"public_ip": "52.0.0.1", "private_ip": "10.0.0.5",
	} {
		if got := m.Attr(k); got != want {
			t.Errorf("attr %s = %q, want %q", k, got, want)
		}
	}
}

// One failing call costs its own resources and nothing else — the rule the
// whole inventory depends on, since a page that shows eleven of twelve
// classes is worth far more than one that shows none.
func TestEC2PartialFailureCostsOnlyItsOwnCall(t *testing.T) {
	f := &fakeEC2{
		instances: &ec2.DescribeInstancesOutput{Reservations: []ec2types.Reservation{{
			Instances: []ec2types.Instance{{InstanceId: aws.String("i-123")}},
		}}},
		errVols: errors.New("Throttling: rate exceeded"),
	}
	rs, partial := ec2Resources(context.Background(), f, "eu-west-1")
	if find(rs, infraview.KindMachine, "i-123") == nil {
		t.Fatalf("a failed volume read must not cost the instances")
	}
	if len(partial) != 1 {
		t.Fatalf("partial = %v, want exactly the volumes line", partial)
	}
	if !contains(partial[0], "volumes") || !contains(partial[0], "eu-west-1") {
		t.Errorf("partial line %q must name what failed and where", partial[0])
	}
}

func contains(s, sub string) bool { return len(s) >= len(sub) && (stringIndex(s, sub) >= 0) }
func stringIndex(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}
  • [ ] Step 3: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/awsinv/ -count=1 Expected: FAIL — undefined: ec2Resources.

  • [ ] Step 4: Write clients.go
// Package awsinv discovers what an AWS account is carrying and reports it
// as an internal/infraview.Inventory. It is the ONLY package in this repo
// that imports an AWS SDK for inventory purposes, which is the whole point
// of the seam: the console page that renders an estate never sees an AWS
// type, so a second provider is a new package rather than a rewrite.
//
// Every call here is read-only. The IAM policy this package needs
// (infra/modules/region-host/main.tf) contains no write action, so that
// property is enforced by the credential and not by this comment.
package awsinv

import (
	"context"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/acm"
	"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
	"github.com/aws/aws-sdk-go-v2/service/route53"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

// The narrow interfaces exist so the mappings can be tested against fakes
// with no network and no credential. Each lists exactly the calls this
// package makes and nothing else — an interface that named the whole
// client would be an invitation to reach for a write.
type EC2API interface {
	DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
	DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error)
	DescribeAddresses(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error)
	DescribeSecurityGroups(context.Context, *ec2.DescribeSecurityGroupsInput, ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error)
	DescribeVpcs(context.Context, *ec2.DescribeVpcsInput, ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error)
	DescribeSubnets(context.Context, *ec2.DescribeSubnetsInput, ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error)
	DescribeRegions(context.Context, *ec2.DescribeRegionsInput, ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error)
}

type S3API interface {
	ListBuckets(context.Context, *s3.ListBucketsInput, ...func(*s3.Options)) (*s3.ListBucketsOutput, error)
	GetBucketLocation(context.Context, *s3.GetBucketLocationInput, ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error)
	GetBucketVersioning(context.Context, *s3.GetBucketVersioningInput, ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error)
	GetBucketEncryption(context.Context, *s3.GetBucketEncryptionInput, ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error)
	GetPublicAccessBlock(context.Context, *s3.GetPublicAccessBlockInput, ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error)
}

type Route53API interface {
	ListHostedZones(context.Context, *route53.ListHostedZonesInput, ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error)
	ListResourceRecordSets(context.Context, *route53.ListResourceRecordSetsInput, ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error)
}

type IAMAPI interface {
	ListRoles(context.Context, *iam.ListRolesInput, ...func(*iam.Options)) (*iam.ListRolesOutput, error)
	ListUsers(context.Context, *iam.ListUsersInput, ...func(*iam.Options)) (*iam.ListUsersOutput, error)
	ListAttachedRolePolicies(context.Context, *iam.ListAttachedRolePoliciesInput, ...func(*iam.Options)) (*iam.ListAttachedRolePoliciesOutput, error)
}

type CloudWatchAPI interface {
	DescribeAlarms(context.Context, *cloudwatch.DescribeAlarmsInput, ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error)
}

type ACMAPI interface {
	ListCertificates(context.Context, *acm.ListCertificatesInput, ...func(*acm.Options)) (*acm.ListCertificatesOutput, error)
}

type TaggingAPI interface {
	GetResources(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error)
}

// Clients is one region's worth of read-only clients. S3/Route53/IAM are
// global services; the assembly calls them once, from the home region's
// Clients, rather than once per region.
type Clients struct {
	EC2        EC2API
	S3         S3API
	Route53    Route53API
	IAM        IAMAPI
	CloudWatch CloudWatchAPI
	ACM        ACMAPI
	Tagging    TaggingAPI
}

// Factory builds the clients for one region. Tests inject a fake; the real
// one below rebases a single resolved aws.Config onto each region so the
// credential chain resolves exactly once.
type Factory func(region string) Clients

func factoryFor(cfg aws.Config) Factory {
	return func(region string) Clients {
		c := cfg.Copy()
		if region != "" {
			c.Region = region
		}
		return Clients{
			EC2:        ec2.NewFromConfig(c),
			S3:         s3.NewFromConfig(c),
			Route53:    route53.NewFromConfig(c),
			IAM:        iam.NewFromConfig(c),
			CloudWatch: cloudwatch.NewFromConfig(c),
			ACM:        acm.NewFromConfig(c),
			Tagging:    resourcegroupstaggingapi.NewFromConfig(c),
		}
	}
}
  • [ ] Step 5: Write ec2.go
package awsinv

import (
	"context"
	"fmt"
	"strconv"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/carlosframework/platform/internal/infraview"
)

// res builds one Resource with the fields every mapping sets the same way,
// so a new mapping cannot forget the provider stamp.
func res(kind, nativeType, id, name, region string) infraview.Resource {
	return infraview.Resource{
		Provider: "aws", Kind: kind, NativeType: nativeType,
		ID: id, Name: name, Region: region, Attrs: map[string]string{},
	}
}

// set writes an attribute only when it has a value: an absent key and an
// empty string are different claims, and the joins treat "" as absence.
func set(r *infraview.Resource, key, value string) {
	if value != "" {
		r.Attrs[key] = value
	}
}

func tagsOf(tags []ec2types.Tag) map[string]string {
	if len(tags) == 0 {
		return nil
	}
	out := map[string]string{}
	for _, t := range tags {
		out[aws.ToString(t.Key)] = aws.ToString(t.Value)
	}
	return out
}

// ec2Resources maps one region's EC2 surface. Each call contributes its own
// resources and, on failure, one line to partial — never an error: this
// function's whole contract is that a throttled volumes call costs the
// volumes and nothing else.
func ec2Resources(ctx context.Context, api EC2API, region string) (out []infraview.Resource, partial []string) {
	fail := func(what string, err error) {
		partial = append(partial, fmt.Sprintf("%s in %s: %v", what, region, err))
	}

	if o, err := api.DescribeInstances(ctx, &ec2.DescribeInstancesInput{}); err != nil {
		fail("instances", err)
	} else {
		for _, rv := range o.Reservations {
			for _, in := range rv.Instances {
				tags := tagsOf(in.Tags)
				r := res(infraview.KindMachine, "ec2:instance", aws.ToString(in.InstanceId), tags["Name"], region)
				r.Tags = tags
				set(&r, "type", string(in.InstanceType))
				if in.State != nil {
					set(&r, "state", string(in.State.Name))
				}
				if in.Placement != nil {
					set(&r, "az", aws.ToString(in.Placement.AvailabilityZone))
				}
				set(&r, "public_ip", aws.ToString(in.PublicIpAddress))
				set(&r, "private_ip", aws.ToString(in.PrivateIpAddress))
				if in.LaunchTime != nil {
					set(&r, "launched", in.LaunchTime.UTC().Format("2006-01-02"))
				}
				out = append(out, r)
			}
		}
	}

	if o, err := api.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{}); err != nil {
		fail("volumes", err)
	} else {
		for _, v := range o.Volumes {
			r := res(infraview.KindDisk, "ec2:volume", aws.ToString(v.VolumeId), tagsOf(v.Tags)["Name"], region)
			r.Tags = tagsOf(v.Tags)
			if v.Size != nil {
				set(&r, "size_gb", strconv.Itoa(int(*v.Size)))
			}
			set(&r, "volume_type", string(v.VolumeType))
			for _, a := range v.Attachments {
				set(&r, "attached_to", aws.ToString(a.InstanceId))
			}
			out = append(out, r)
		}
	}

	if o, err := api.DescribeAddresses(ctx, &ec2.DescribeAddressesInput{}); err != nil {
		fail("elastic IPs", err)
	} else {
		for _, a := range o.Addresses {
			r := res(infraview.KindIP, "ec2:eip", aws.ToString(a.AllocationId), tagsOf(a.Tags)["Name"], region)
			r.Tags = tagsOf(a.Tags)
			set(&r, "address", aws.ToString(a.PublicIp))
			set(&r, "attached_to", aws.ToString(a.InstanceId))
			out = append(out, r)
		}
	}

	if o, err := api.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{}); err != nil {
		fail("security groups", err)
	} else {
		for _, g := range o.SecurityGroups {
			r := res(infraview.KindFirewall, "ec2:security-group", aws.ToString(g.GroupId), aws.ToString(g.GroupName), region)
			r.Tags = tagsOf(g.Tags)
			set(&r, "ingress", summarizePerms(g.IpPermissions))
			set(&r, "egress", summarizePerms(g.IpPermissionsEgress))
			out = append(out, r)
		}
	}

	if o, err := api.DescribeVpcs(ctx, &ec2.DescribeVpcsInput{}); err != nil {
		fail("VPCs", err)
	} else {
		for _, v := range o.Vpcs {
			r := res(infraview.KindNetwork, "ec2:vpc", aws.ToString(v.VpcId), tagsOf(v.Tags)["Name"], region)
			r.Tags = tagsOf(v.Tags)
			set(&r, "cidr", aws.ToString(v.CidrBlock))
			out = append(out, r)
		}
	}

	if o, err := api.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{}); err != nil {
		fail("subnets", err)
	} else {
		for _, s := range o.Subnets {
			r := res(infraview.KindNetwork, "ec2:subnet", aws.ToString(s.SubnetId), tagsOf(s.Tags)["Name"], region)
			r.Tags = tagsOf(s.Tags)
			set(&r, "cidr", aws.ToString(s.CidrBlock))
			set(&r, "az", aws.ToString(s.AvailabilityZone))
			set(&r, "vpc", aws.ToString(s.VpcId))
			out = append(out, r)
		}
	}
	return out, partial
}

// summarizePerms renders a rule set as "tcp/22 from 10.0.0.0/8 · tcp/443
// from 0.0.0.0/0" — a sentence an operator reads, never the raw JSON, which
// would be both unreadable and a wall of detail the page does not want.
func summarizePerms(perms []ec2types.IpPermission) string {
	out := ""
	for _, p := range perms {
		proto := aws.ToString(p.IpProtocol)
		if proto == "-1" {
			proto = "all"
		}
		port := "all"
		if p.FromPort != nil {
			port = strconv.Itoa(int(*p.FromPort))
			if p.ToPort != nil && *p.ToPort != *p.FromPort {
				port += "-" + strconv.Itoa(int(*p.ToPort))
			}
		}
		for _, rng := range p.IpRanges {
			if out != "" {
				out += " · "
			}
			out += fmt.Sprintf("%s/%s from %s", proto, port, aws.ToString(rng.CidrIp))
		}
	}
	return out
}
  • [ ] Step 6: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/awsinv/ -count=1 -v Expected: PASS — TestEC2InstanceMapping, TestEC2PartialFailureCostsOnlyItsOwnCall.

  • [ ] Step 7: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
gofmt -l internal/awsinv
git add go.mod go.sum internal/awsinv
git commit -m "awsinv: read-only client seam and EC2 mappings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 3: internal/awsinv — remaining services, catch-all, assembly

Files:

  • Create: internal/awsinv/rest.go, internal/awsinv/awsinv.go
  • Test: internal/awsinv/rest_test.go, internal/awsinv/awsinv_test.go

Interfaces:

  • Consumes: Clients, Factory, ec2Resources (Task 2), infraview.Inventory.
  • Produces: awsinv.New(ctx context.Context) (*Provider, error), (*Provider).Inventory(ctx) (infraview.Inventory, error), and for tests newWithFactory(f Factory, home string, regions []string, account string) *Provider.
  • [ ] Step 1: Write the failing assembly test
package awsinv

import (
	"context"
	"errors"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
	rgtypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
	"github.com/carlosframework/platform/internal/infraview"
)

type fakeTagging struct {
	out *resourcegroupstaggingapi.GetResourcesOutput
	err error
}

func (f *fakeTagging) GetResources(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
	return f.out, f.err
}

// A resource the typed mappings already produced must NOT appear twice
// because the catch-all also saw it; anything the catch-all alone knows
// about must appear as "other" rather than vanishing.
func TestCatchAllDedupesAgainstTypedResources(t *testing.T) {
	factory := func(region string) Clients {
		return Clients{
			EC2: &fakeEC2{instances: &ec2.DescribeInstancesOutput{
				Reservations: []ec2types.Reservation{{Instances: []ec2types.Instance{{
					InstanceId: aws.String("i-123"),
				}}}},
			}},
			Tagging: &fakeTagging{out: &resourcegroupstaggingapi.GetResourcesOutput{
				ResourceTagMappingList: []rgtypes.ResourceTagMapping{
					{ResourceARN: aws.String("arn:aws:ec2:eu-west-1:1234:instance/i-123")},
					{ResourceARN: aws.String("arn:aws:sqs:eu-west-1:1234:some-queue")},
				},
			}},
		}
	}
	p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "1234")

	inv, err := p.Inventory(context.Background())
	if err != nil {
		t.Fatalf("Inventory: %v", err)
	}
	machines := inv.ByKind(infraview.KindMachine)
	if len(machines) != 1 {
		t.Fatalf("machines = %d, want 1 (the catch-all must not duplicate a typed resource)", len(machines))
	}
	others := inv.ByKind(infraview.KindOther)
	if len(others) != 1 || others[0].NativeType != "sqs" {
		t.Fatalf("others = %+v, want one sqs resource", others)
	}
	if inv.Provider != "aws" || inv.Account != "1234" || inv.TakenAt.IsZero() {
		t.Fatalf("header not stamped: %+v", inv)
	}
}

// Auth-shaped failure with nothing discovered is the one case that must
// reach the caller as an error — it is what latches the console's pass off
// so an ungranted deployment says "not granted" instead of erroring hourly.
func TestAuthShapedWithNoResourcesIsAnError(t *testing.T) {
	authErr := &fakeAPIErr{code: "AccessDenied"}
	factory := func(region string) Clients {
		return Clients{
			EC2:     &fakeEC2Failing{err: authErr},
			Tagging: &fakeTagging{err: authErr},
		}
	}
	p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "")
	if _, err := p.Inventory(context.Background()); err == nil {
		t.Fatal("want an error when every read is denied and nothing was discovered")
	}
}

// The same denial, but something WAS discovered: the inventory is returned
// with the failures named in Partial, never thrown away.
func TestPartialSurvivesWhenSomethingWasDiscovered(t *testing.T) {
	factory := func(region string) Clients {
		return Clients{
			EC2: &fakeEC2{instances: &ec2.DescribeInstancesOutput{
				Reservations: []ec2types.Reservation{{Instances: []ec2types.Instance{{
					InstanceId: aws.String("i-123"),
				}}}},
				errVols: errors.New("AccessDenied: not authorized"),
			}},
			Tagging: &fakeTagging{err: errors.New("AccessDenied: not authorized")},
		}
	}
	p := newWithFactory(factory, "eu-west-1", []string{"eu-west-1"}, "")
	inv, err := p.Inventory(context.Background())
	if err != nil {
		t.Fatalf("Inventory: %v", err)
	}
	if len(inv.Partial) == 0 {
		t.Fatal("failures must be named in Partial")
	}
}

Add the two small fakes this test needs to rest_test.go:

type fakeAPIErr struct{ code string }

func (e *fakeAPIErr) Error() string                 { return e.code }
func (e *fakeAPIErr) ErrorCode() string             { return e.code }
func (e *fakeAPIErr) ErrorMessage() string          { return e.code }
func (e *fakeAPIErr) ErrorFault() smithy.ErrorFault { return smithy.FaultClient }

// fakeEC2Failing fails every call with the same error.
type fakeEC2Failing struct{ err error }

func (f *fakeEC2Failing) DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
	return nil, f.err
}
// … the remaining six EC2API methods, each `return nil, f.err`
  • [ ] Step 2: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/awsinv/ -count=1 Expected: FAIL — undefined: newWithFactory.

  • [ ] Step 3: Write rest.go

Mappings, each following ec2Resources' contract exactly (own resources, own partial line, never an error):

package awsinv

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/acm"
	"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
	"github.com/aws/aws-sdk-go-v2/service/route53"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/carlosframework/platform/internal/infraview"
)

// joinedRecordTypes are the record types that can participate in the
// DNS -> IP -> machine join, and so the only ones materialised as their own
// Resource. Everything else is counted into the zone's record_count: a zone
// can hold thousands of records and this object is read on every page load.
var joinedRecordTypes = map[string]bool{"A": true, "AAAA": true}

func s3Resources(ctx context.Context, api S3API) (out []infraview.Resource, partial []string) {
	o, err := api.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return nil, []string{fmt.Sprintf("buckets: %v", err)}
	}
	for _, b := range o.Buckets {
		name := aws.ToString(b.Name)
		r := res(infraview.KindBucket, "s3:bucket", name, name, "")
		if loc, err := api.GetBucketLocation(ctx, &s3.GetBucketLocationInput{Bucket: b.Name}); err == nil {
			region := string(loc.LocationConstraint)
			if region == "" {
				region = "us-east-1" // the API's documented empty-means-us-east-1
			}
			r.Region = region
		}
		// Each of the three posture reads is best-effort per bucket: a
		// bucket whose versioning cannot be read still appears, with the
		// attribute absent rather than a false "off". "off" is a claim.
		if v, err := api.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{Bucket: b.Name}); err == nil {
			set(&r, "versioning", strings.ToLower(string(v.Status)))
		}
		if _, err := api.GetBucketEncryption(ctx, &s3.GetBucketEncryptionInput{Bucket: b.Name}); err == nil {
			set(&r, "encryption", "on")
		}
		if p, err := api.GetPublicAccessBlock(ctx, &s3.GetPublicAccessBlockInput{Bucket: b.Name}); err == nil && p.PublicAccessBlockConfiguration != nil {
			c := p.PublicAccessBlockConfiguration
			if aws.ToBool(c.BlockPublicAcls) && aws.ToBool(c.BlockPublicPolicy) {
				set(&r, "public_access_block", "on")
			} else {
				set(&r, "public_access_block", "partial")
			}
		}
		out = append(out, r)
	}
	return out, partial
}

func route53Resources(ctx context.Context, api Route53API) (out []infraview.Resource, partial []string) {
	zones, err := api.ListHostedZones(ctx, &route53.ListHostedZonesInput{})
	if err != nil {
		return nil, []string{fmt.Sprintf("hosted zones: %v", err)}
	}
	for _, z := range zones.HostedZones {
		id := aws.ToString(z.Id)
		name := strings.TrimSuffix(aws.ToString(z.Name), ".")
		zr := res(infraview.KindDNSZone, "route53:hosted-zone", id, name, "")
		if z.Config != nil && z.Config.PrivateZone {
			set(&zr, "private", "yes")
		}
		count := 0
		var next *string
		for {
			recs, err := api.ListResourceRecordSets(ctx, &route53.ListResourceRecordSetsInput{
				HostedZoneId: z.Id, StartRecordName: next,
			})
			if err != nil {
				partial = append(partial, fmt.Sprintf("records in %s: %v", name, err))
				break
			}
			for _, rec := range recs.ResourceRecordSets {
				count++
				rtype := string(rec.Type)
				if !joinedRecordTypes[rtype] {
					continue
				}
				rname := strings.TrimSuffix(aws.ToString(rec.Name), ".")
				for _, v := range rec.ResourceRecords {
					rr := res(infraview.KindDNSRecord, "route53:record", rname+"/"+rtype+"/"+aws.ToString(v.Value), rname, "")
					set(&rr, "zone", name)
					set(&rr, "type", rtype)
					set(&rr, "value", aws.ToString(v.Value))
					out = append(out, rr)
				}
				if rec.AliasTarget != nil {
					rr := res(infraview.KindDNSRecord, "route53:alias", rname+"/ALIAS", rname, "")
					set(&rr, "zone", name)
					set(&rr, "type", "ALIAS")
					set(&rr, "value", strings.TrimSuffix(aws.ToString(rec.AliasTarget.DNSName), "."))
					out = append(out, rr)
				}
			}
			if !recs.IsTruncated {
				break
			}
			next = recs.NextRecordName
		}
		set(&zr, "record_count", strconv.Itoa(count))
		out = append(out, zr)
	}
	return out, partial
}

// iamResources reports identities as NAMES and a count of attached
// policies. Policy documents are deliberately never read: this page is a
// map, not a credential viewer (spec §4).
func iamResources(ctx context.Context, api IAMAPI) (out []infraview.Resource, partial []string) {
	if o, err := api.ListRoles(ctx, &iam.ListRolesInput{}); err != nil {
		partial = append(partial, fmt.Sprintf("IAM roles: %v", err))
	} else {
		for _, role := range o.Roles {
			name := aws.ToString(role.RoleName)
			r := res(infraview.KindIdentity, "iam:role", aws.ToString(role.Arn), name, "")
			set(&r, "kind", "role")
			if pol, err := api.ListAttachedRolePolicies(ctx, &iam.ListAttachedRolePoliciesInput{RoleName: role.RoleName}); err == nil {
				set(&r, "attached_policies", strconv.Itoa(len(pol.AttachedPolicies)))
			}
			out = append(out, r)
		}
	}
	if o, err := api.ListUsers(ctx, &iam.ListUsersInput{}); err != nil {
		partial = append(partial, fmt.Sprintf("IAM users: %v", err))
	} else {
		for _, u := range o.Users {
			r := res(infraview.KindIdentity, "iam:user", aws.ToString(u.Arn), aws.ToString(u.UserName), "")
			set(&r, "kind", "user")
			out = append(out, r)
		}
	}
	return out, partial
}

func alarmResources(ctx context.Context, api CloudWatchAPI, region string) (out []infraview.Resource, partial []string) {
	o, err := api.DescribeAlarms(ctx, &cloudwatch.DescribeAlarmsInput{})
	if err != nil {
		return nil, []string{fmt.Sprintf("alarms in %s: %v", region, err)}
	}
	for _, a := range o.MetricAlarms {
		r := res(infraview.KindAlarm, "cloudwatch:alarm", aws.ToString(a.AlarmArn), aws.ToString(a.AlarmName), region)
		set(&r, "state", string(a.StateValue))
		set(&r, "metric", aws.ToString(a.MetricName))
		for _, d := range a.Dimensions {
			if aws.ToString(d.Name) == "InstanceId" {
				set(&r, "target", aws.ToString(d.Value))
			}
		}
		out = append(out, r)
	}
	return out, partial
}

func certResources(ctx context.Context, api ACMAPI, region string) (out []infraview.Resource, partial []string) {
	o, err := api.ListCertificates(ctx, &acm.ListCertificatesInput{})
	if err != nil {
		return nil, []string{fmt.Sprintf("certificates in %s: %v", region, err)}
	}
	for _, c := range o.CertificateSummaryList {
		r := res(infraview.KindCertificate, "acm:certificate", aws.ToString(c.CertificateArn), aws.ToString(c.DomainName), region)
		set(&r, "domain", aws.ToString(c.DomainName))
		set(&r, "status", string(c.Status))
		if c.NotAfter != nil {
			set(&r, "expires", c.NotAfter.UTC().Format("2006-01-02"))
		}
		out = append(out, r)
	}
	return out, partial
}

// catchAll is the one genuinely AWS-shaped trick in this package: one call
// that returns every TAGGABLE resource in the region, so a resource type
// nobody wrote a mapping for is still visible. seen holds the ids the typed
// mappings already produced — an ARN whose id is in it is skipped, so the
// catch-all only ever ADDS.
func catchAll(ctx context.Context, api TaggingAPI, region string, seen map[string]bool) (out []infraview.Resource, partial []string) {
	var token *string
	for {
		o, err := api.GetResources(ctx, &resourcegroupstaggingapi.GetResourcesInput{PaginationToken: token})
		if err != nil {
			return out, append(partial, fmt.Sprintf("resource inventory in %s: %v", region, err))
		}
		for _, m := range o.ResourceTagMappingList {
			arn := aws.ToString(m.ResourceARN)
			service, id := parseARN(arn)
			if id == "" || seen[id] || seen[arn] {
				continue
			}
			r := res(infraview.KindOther, service, arn, id, region)
			for _, t := range m.Tags {
				if r.Tags == nil {
					r.Tags = map[string]string{}
				}
				r.Tags[aws.ToString(t.Key)] = aws.ToString(t.Value)
			}
			out = append(out, r)
		}
		if o.PaginationToken == nil || *o.PaginationToken == "" {
			return out, partial
		}
		token = o.PaginationToken
	}
}

// parseARN pulls the service and the trailing resource id out of an ARN:
// arn:aws:ec2:eu-west-1:1234:instance/i-123 -> ("ec2", "i-123").
func parseARN(arn string) (service, id string) {
	parts := strings.SplitN(arn, ":", 6)
	if len(parts) < 6 {
		return "", ""
	}
	service = parts[2]
	tail := parts[5]
	if i := strings.LastIndexAny(tail, "/:"); i >= 0 {
		tail = tail[i+1:]
	}
	return service, tail
}
  • [ ] Step 4: Write awsinv.go (assembly)
package awsinv

import (
	"context"
	"errors"
	"fmt"
	"time"

	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/sts"
	"github.com/aws/smithy-go"
	"github.com/carlosframework/platform/internal/infraview"
)

// credentialProbe is how long New waits for the credential chain to answer
// before deciding this deployment has none. Five seconds, matching
// cepull.go's own bounded probe: on a box with an instance role it is
// instant, and on a laptop with no credential at all the chain can
// otherwise hang on IMDS.
const credentialProbe = 5 * time.Second

// Provider is the AWS implementation of infraview.Provider.
type Provider struct {
	factory Factory
	home    string   // the region whose clients answer the global services
	regions []string // every region to inventory
	account string
	now     func() time.Time // test seam
}

// New resolves the credential chain once, discovers the account's
// opted-in regions, and returns a Provider. Every failure here is
// construction-class — no config, no credential, no reachable EC2 — and the
// caller (the console pass) treats an auth-shaped one as "this deployment
// has no inventory grant", latching the pass off rather than retrying
// hourly forever.
func New(ctx context.Context) (*Provider, error) {
	cfg, err := awsconfig.LoadDefaultConfig(ctx)
	if err != nil {
		return nil, fmt.Errorf("awsinv: aws config: %w", err)
	}
	probeCtx, cancel := context.WithTimeout(ctx, credentialProbe)
	defer cancel()
	if _, err := cfg.Credentials.Retrieve(probeCtx); err != nil {
		return nil, fmt.Errorf("awsinv: no usable credential: %w", err)
	}
	factory := factoryFor(cfg)
	home := cfg.Region
	if home == "" {
		home = "us-east-1"
	}

	regions := []string{home}
	if o, err := factory(home).EC2.DescribeRegions(ctx, &ec2.DescribeRegionsInput{}); err != nil {
		if IsAuthShaped(err) {
			return nil, fmt.Errorf("awsinv: describe regions: %w", err)
		}
		// Not auth-shaped: inventory the home region alone rather than
		// nothing. The missing regions surface in Partial at assembly.
	} else {
		regions = regions[:0]
		for _, r := range o.Regions {
			if r.RegionName != nil {
				regions = append(regions, *r.RegionName)
			}
		}
	}

	account := ""
	if id, err := sts.NewFromConfig(cfg).GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}); err == nil && id.Account != nil {
		account = *id.Account
	}
	return &Provider{factory: factory, home: home, regions: regions, account: account, now: time.Now}, nil
}

// newWithFactory is New's test twin: no credential chain, no network.
func newWithFactory(f Factory, home string, regions []string, account string) *Provider {
	return &Provider{factory: f, home: home, regions: regions, account: account, now: time.Now}
}

// Inventory walks every region's typed mappings, then the catch-all, then
// the global services once from the home region.
//
// The error contract: an error means "this deployment cannot inventory
// itself at all". Anything less — a throttled call, one dead region, a
// service the role cannot read — lands in Partial and the inventory is
// still returned, because a page showing eleven of twelve resource classes
// is worth far more than an error.
func (p *Provider) Inventory(ctx context.Context) (infraview.Inventory, error) {
	inv := infraview.Inventory{Provider: "aws", Account: p.account, TakenAt: p.now().UTC()}
	seen := map[string]bool{}
	authDenied := false

	note := func(lines []string) {
		inv.Partial = append(inv.Partial, lines...)
	}
	add := func(rs []infraview.Resource) {
		for _, r := range rs {
			seen[r.ID] = true
			inv.Resources = append(inv.Resources, r)
		}
	}

	for _, region := range p.regions {
		c := p.factory(region)
		rs, partial := ec2Resources(ctx, c.EC2, region)
		add(rs)
		note(partial)

		rs, partial = alarmResources(ctx, c.CloudWatch, region)
		add(rs)
		note(partial)

		rs, partial = certResources(ctx, c.ACM, region)
		add(rs)
		note(partial)

		rs, partial = catchAll(ctx, c.Tagging, region, seen)
		add(rs)
		note(partial)
	}

	global := p.factory(p.home)
	rs, partial := s3Resources(ctx, global.S3)
	add(rs)
	note(partial)

	rs, partial = route53Resources(ctx, global.Route53)
	add(rs)
	note(partial)

	rs, partial = iamResources(ctx, global.IAM)
	add(rs)
	note(partial)

	for _, line := range inv.Partial {
		if looksDenied(line) {
			authDenied = true
			break
		}
	}
	if len(inv.Resources) == 0 && authDenied {
		return inv, errors.New("awsinv: every read was denied — this deployment has no inventory grant")
	}
	return inv, nil
}

// IsAuthShaped is cepull.go's isAuthShapedError, exported here so the
// console classifies THIS package's construction errors by the same rule it
// classifies Cost Explorer's. One rule, one list of codes.
func IsAuthShaped(err error) bool {
	var apiErr smithy.APIError
	if !errors.As(err, &apiErr) {
		return false
	}
	return looksDenied(apiErr.ErrorCode())
}

func looksDenied(s string) bool {
	for _, shape := range []string{
		"AccessDenied", "UnauthorizedOperation", "UnrecognizedClient",
		"InvalidClientTokenId", "InvalidAccessKeyId", "AuthFailure", "ExpiredToken",
	} {
		if strings.Contains(s, shape) {
			return true
		}
	}
	return false
}
  • [ ] Step 5: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/awsinv/ -count=1 -v Expected: PASS — all five tests, including the three from this task.

  • [ ] Step 6: Add the remaining mapping tests

One focused test per mapping in rest_test.go, each with a fake returning one item and asserting the produced Resource. TestS3BucketMapping is the pattern the rest follow:

type fakeS3 struct {
	buckets *s3.ListBucketsOutput
	vers    *s3.GetBucketVersioningOutput
}

func (f *fakeS3) ListBuckets(context.Context, *s3.ListBucketsInput, ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
	return f.buckets, nil
}
func (f *fakeS3) GetBucketLocation(context.Context, *s3.GetBucketLocationInput, ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) {
	return &s3.GetBucketLocationOutput{LocationConstraint: s3types.BucketLocationConstraintEuWest1}, nil
}
func (f *fakeS3) GetBucketVersioning(context.Context, *s3.GetBucketVersioningInput, ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error) {
	return f.vers, nil
}
func (f *fakeS3) GetBucketEncryption(context.Context, *s3.GetBucketEncryptionInput, ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error) {
	return nil, errors.New("no encryption configured")
}
func (f *fakeS3) GetPublicAccessBlock(context.Context, *s3.GetPublicAccessBlockInput, ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
	return nil, errors.New("no public access block")
}

// A bucket whose posture reads fail still appears — with the attribute
// ABSENT, never a false "off". "Off" is a claim this read did not earn.
func TestS3BucketMapping(t *testing.T) {
	f := &fakeS3{
		buckets: &s3.ListBucketsOutput{Buckets: []s3types.Bucket{{Name: aws.String("carlos-data-ie")}}},
		vers:    &s3.GetBucketVersioningOutput{Status: s3types.BucketVersioningStatusEnabled},
	}
	rs, partial := s3Resources(context.Background(), f)
	if len(partial) != 0 || len(rs) != 1 {
		t.Fatalf("rs=%+v partial=%v", rs, partial)
	}
	b := rs[0]
	if b.Kind != infraview.KindBucket || b.Name != "carlos-data-ie" || b.Region != "eu-west-1" {
		t.Errorf("bucket = %+v", b)
	}
	if b.Attr("versioning") != "enabled" {
		t.Errorf("versioning = %q, want enabled", b.Attr("versioning"))
	}
	if _, ok := b.Attrs["encryption"]; ok {
		t.Errorf("a failed encryption read must leave the attribute absent, not claim a value")
	}
}

Then the remaining ones, same shape: TestS3BucketMapping (name, region, versioning/encryption/PAB attrs), TestRoute53RecordsMaterialiseOnlyJoinable (an A record becomes a dns_record; a TXT record only bumps record_count), TestIAMNeverCarriesPolicyDocuments (assert no attr value contains "Statement"), TestAlarmTargetsInstanceDimension, TestCertMapping, TestParseARN (table-driven, including a malformed ARN returning ("", "")).

  • [ ] Step 7: Run the full package tests

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/awsinv/ -count=1 Expected: PASS.

  • [ ] Step 8: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
gofmt -l internal/awsinv
git add internal/awsinv go.mod go.sum
git commit -m "awsinv: S3/Route53/IAM/CloudWatch/ACM mappings, tagging catch-all, assembly

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 4: The console inventory pass

Files:

  • Create: internal/console/infrapass.go, internal/console/infrapass_test.go
  • Modify: internal/console/server.go (Server fields), cmd/console/main.go:273 (start the pass)

Interfaces:

  • Consumes: infraview.Inventory, infraview.InventoryKey, awsinv.New, awsinv.IsAuthShaped, release.Store.
  • Produces: (*Server).StartInfraPass(ctx context.Context), (*Server).infraPass(ctx context.Context) error, (*Server).loadInventory(ctx context.Context) (*infraview.Inventory, string) where the string is a human-readable reason when the inventory is nil ("never", "ungranted", or a read error), and the Server seam field inventory func(ctx context.Context) (infraview.Inventory, error).
  • [ ] Step 1: Write the failing test
package console

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/infraview"
)

// The pass writes the snapshot where the page reads it, and loadInventory
// reads back exactly what was written.
func TestInfraPassWritesSnapshot(t *testing.T) {
	fx := newConsoleFixture(t)
	fx.srv.inventory = func(context.Context) (infraview.Inventory, error) {
		return infraview.Inventory{
			Provider: "aws", Account: "1234", TakenAt: time.Now().UTC(),
			Resources: []infraview.Resource{{Provider: "aws", Kind: infraview.KindMachine, ID: "i-1"}},
		}, nil
	}
	if err := fx.srv.infraPass(context.Background()); err != nil {
		t.Fatalf("infraPass: %v", err)
	}
	body, _, err := fx.rel.Get(context.Background(), infraview.InventoryKey)
	if err != nil {
		t.Fatalf("read back: %v", err)
	}
	var inv infraview.Inventory
	if err := json.Unmarshal(body, &inv); err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(inv.Resources) != 1 || inv.Resources[0].ID != "i-1" {
		t.Fatalf("stored = %+v", inv)
	}
	got, why := fx.srv.loadInventory(context.Background())
	if got == nil || why != "" {
		t.Fatalf("loadInventory = %+v, %q; want the snapshot", got, why)
	}
}

// An auth-shaped failure latches the pass off for the process after ONE
// attempt: "this deployment has no inventory grant" is a permanent answer,
// not something to re-ask AWS every hour forever.
func TestInfraPassLatchesOnAuthShapedFailure(t *testing.T) {
	fx := newConsoleFixture(t)
	calls := 0
	fx.srv.inventory = func(context.Context) (infraview.Inventory, error) {
		calls++
		return infraview.Inventory{}, &fakeAPIError{code: "AccessDenied"}
	}
	if err := fx.srv.infraPass(context.Background()); err == nil {
		t.Fatal("want an error from the denied pass")
	}
	if err := fx.srv.infraPass(context.Background()); err == nil {
		t.Fatal("want an error from the latched pass")
	}
	if calls != 1 {
		t.Fatalf("provider called %d times, want 1 — the latch must stop the second attempt", calls)
	}
	if _, why := fx.srv.loadInventory(context.Background()); why != "ungranted" {
		t.Fatalf("loadInventory reason = %q, want %q", why, "ungranted")
	}
}

// A transient failure is NOT the latch: CE throttles under load and a blip
// must not silently kill inventory until the next redeploy.
func TestInfraPassRetriesTransientFailure(t *testing.T) {
	fx := newConsoleFixture(t)
	calls := 0
	fx.srv.inventory = func(context.Context) (infraview.Inventory, error) {
		calls++
		return infraview.Inventory{}, errors.New("Throttling: rate exceeded")
	}
	_ = fx.srv.infraPass(context.Background())
	_ = fx.srv.infraPass(context.Background())
	if calls != 2 {
		t.Fatalf("provider called %d times, want 2 — a throttle must not latch", calls)
	}
}

// With no snapshot ever written, the page must be told "never", never
// handed an empty inventory that would render as "this account has no
// resources".
func TestLoadInventoryNeverTaken(t *testing.T) {
	fx := newConsoleFixture(t)
	inv, why := fx.srv.loadInventory(context.Background())
	if inv != nil || why != "never" {
		t.Fatalf("loadInventory = %+v, %q; want nil, \"never\"", inv, why)
	}
}

Add fakeAPIError to infrapass_test.go (the same four-method smithy.APIError shape as Task 3's fakeAPIErr).

  • [ ] Step 2: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run TestInfraPass -count=1 Expected: FAIL — fx.srv.inventory undefined.

  • [ ] Step 3: Add the Server fields

In internal/console/server.go, beside the existing ceCosts/ceDisabled/ceWarnOnce seam fields on the Server struct:

	// The infrastructure inventory seam (infrapass.go), built exactly like
	// the Cost Explorer seam above it: tests set inventory to a fake, the
	// real path lazily constructs an awsinv.Provider once. infraDisabled
	// latches an auth-shaped failure off for the process — a deployment
	// with no inventory IAM grant answers "ungranted" once, not hourly.
	inventory       func(ctx context.Context) (infraview.Inventory, error)
	infraOnce       sync.Once
	infraProvider   infraview.Provider
	infraDisabled   bool
	infraRefreshing atomic.Bool
  • [ ] Step 4: Write infrapass.go
// The infrastructure inventory pass: once an hour, ask the deployment's
// cloud provider what it is carrying and write the answer to
// infraview.InventoryKey in the deployment bucket, where
// /fleet/architecture reads it.
//
// The failure posture is cepull.go's, verbatim and for the same reasons.
// Construction-class failures — no AWS config, no usable credential, an
// AccessDenied-shaped answer to the first calls — latch the pass off for
// the process after a single log line. There is no "the IAM grant might
// appear later" retry worth making, and a deployment without the grant must
// show "not granted" rather than filling its log with an hourly error.
// Everything else — throttling, a 5xx, a network blip — logs and the next
// tick tries again.
package console

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/carlosframework/platform/internal/awsinv"
	"github.com/carlosframework/platform/internal/infraview"
)

// infraPassInterval matches economicsPassInterval: an estate changes on the
// timescale of a deploy, not a request, and the refresh button covers the
// moment an operator actually needs it now.
const infraPassInterval = time.Hour

// infraPassTimeout bounds one whole inventory — every region, every
// service. A pass that hangs must not hold the goroutine until the process
// restarts.
const infraPassTimeout = 2 * time.Minute

// StartInfraPass runs infraPass immediately and then hourly until ctx is
// done — StartEconomicsPass's shape (economicspass.go:38).
func (s *Server) StartInfraPass(ctx context.Context) {
	if s.cfg.Releases == nil {
		return // no bucket, nowhere to write, nothing to do
	}
	t := time.NewTicker(infraPassInterval)
	defer t.Stop()
	for {
		if err := s.infraPass(ctx); err != nil {
			log.Printf("console: infra pass: %v", err)
		}
		select {
		case <-ctx.Done():
			return
		case <-t.C:
		}
	}
}

// resolveInventory is the seam: the test fake when one is set, otherwise a
// lazily constructed awsinv.Provider reused for the life of the process.
func (s *Server) resolveInventory(ctx context.Context) (infraview.Inventory, error) {
	if s.inventory != nil {
		return s.inventory(ctx)
	}
	if s.infraDisabled {
		return infraview.Inventory{}, errors.New("console: infra inventory disabled for this process")
	}
	s.infraOnce.Do(func() {
		p, err := awsinv.New(ctx)
		if err != nil {
			// Construction failed. Auth-shaped means no grant: latch.
			// Anything else means this process cannot build a provider at
			// all, which is equally permanent for this process.
			log.Printf("console: infra inventory unavailable: %v", err)
			s.infraDisabled = true
			return
		}
		s.infraProvider = p
	})
	if s.infraProvider == nil {
		s.infraDisabled = true
		return infraview.Inventory{}, errors.New("console: no infrastructure provider")
	}
	return s.infraProvider.Inventory(ctx)
}

// infraPass takes one inventory and stores it. An auth-shaped failure
// latches; everything else is returned for the caller to log and retry.
func (s *Server) infraPass(ctx context.Context) error {
	if s.infraDisabled {
		return errors.New("console: infra inventory disabled for this process")
	}
	ctx, cancel := context.WithTimeout(ctx, infraPassTimeout)
	defer cancel()

	inv, err := s.resolveInventory(ctx)
	if err != nil {
		if awsinv.IsAuthShaped(err) || isAuthShapedError(err) {
			s.infraDisabled = true
			return fmt.Errorf("console: infra inventory not granted (disabled for this process): %w", err)
		}
		return fmt.Errorf("console: infra inventory: %w", err)
	}
	return s.putInventory(ctx, inv)
}

// putInventory CAS-writes the snapshot — upsertBill's read-etag-then-put
// (economicspass.go:942), with os.ErrNotExist meaning "first ever write".
func (s *Server) putInventory(ctx context.Context, inv infraview.Inventory) error {
	if s.cfg.Releases == nil {
		return nil
	}
	_, etag, err := s.cfg.Releases.Get(ctx, infraview.InventoryKey)
	switch {
	case err == nil:
	case errors.Is(err, os.ErrNotExist):
		etag = ""
	default:
		return fmt.Errorf("console: inventory: read: %w", err)
	}
	body, err := json.Marshal(inv)
	if err != nil {
		return fmt.Errorf("console: inventory: marshal: %w", err)
	}
	if _, err := s.cfg.Releases.Put(ctx, infraview.InventoryKey, body, etag); err != nil {
		return fmt.Errorf("console: inventory: write: %w", err)
	}
	return nil
}

// loadInventory reads the stored snapshot for the page. The second return
// is why there is nothing to show, and it distinguishes the three states
// that must never be rendered as one: "never" (no pass has ever succeeded),
// "ungranted" (this process latched off), and a read error. "no resources"
// is a claim; none of these three earn it.
func (s *Server) loadInventory(ctx context.Context) (*infraview.Inventory, string) {
	if s.cfg.Releases == nil {
		return nil, "never"
	}
	body, _, err := s.cfg.Releases.Get(ctx, infraview.InventoryKey)
	switch {
	case errors.Is(err, os.ErrNotExist):
		if s.infraDisabled {
			return nil, "ungranted"
		}
		return nil, "never"
	case err != nil:
		log.Printf("console: inventory read: %v", err)
		return nil, "unreadable"
	}
	var inv infraview.Inventory
	if err := json.Unmarshal(body, &inv); err != nil {
		log.Printf("console: inventory parse: %v", err)
		return nil, "unreadable"
	}
	return &inv, ""
}
  • [ ] Step 5: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run TestInfraPass -count=1 -v and -run TestLoadInventory Expected: PASS — four tests.

  • [ ] Step 6: Start the pass

In cmd/console/main.go, beside line 273:

	go srv.StartEconomicsPass(context.Background())
	go srv.StartInfraPass(context.Background())
  • [ ] Step 7: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
gofmt -l internal/console cmd/console
git add internal/console/infrapass.go internal/console/infrapass_test.go internal/console/server.go cmd/console/main.go
git commit -m "console: hourly infrastructure inventory pass

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 5: The graph joins, box cards and cost/drift

Files:

  • Create: internal/console/archgraph.go, internal/console/archgraph_test.go

Interfaces:

  • Consumes: infraview.Inventory, status.Fleet, HostRow (view.go:679), registry.Route, economics.RateCard, economics.HoursPerMonth.
  • Produces:
type ArchInput struct {
	Inv    *infraview.Inventory
	Fleet  *status.Fleet
	Hosts  []HostRow
	Routes []registry.Route
	Rates  economics.RateCard
}
type KV struct{ K, V string }
type Node struct {
	ID, Kind, Title, Subtitle string
	Detail                    []KV
	X, Y, W, H                int
}
type Edge struct{ From, To, Kind string }
type BoxCard struct {
	Label, Region, InstanceType, State string
	Cores, MemGB, EBSGB                float64
	DiscoveredDiskGB                   float64
	USDMonth                           float64
	Apps                               []string
	Static, Resident, Hibernating      int
	Awake, Asleep                      *int
	Release, Load                      string
	Stale                              bool
	Dark                               string
	Drift                              []string
	CloudID                            string // "" = no cloud machine behind this box
}
type ResourceGroup struct {
	Title string
	Rows  []ResourceRow
}
type ResourceRow struct{ Name, NativeType, Region, Detail string }
type ArchView struct {
	Nodes         []Node
	Edges         []Edge
	Width, Height int
	Boxes         []BoxCard
	Groups        []ResourceGroup
	TakenAt       time.Time
	Account       string
	Partial       []string
	Missing       string // "" | "never" | "ungranted" | "unreadable"
}
func BuildArch(in ArchInput) ArchView
  • [ ] Step 1: Write the failing join test
package console

import (
	"testing"

	"github.com/carlosframework/platform/internal/economics"
	"github.com/carlosframework/platform/internal/infraview"
	"github.com/carlosframework/platform/internal/registry"
	"github.com/carlosframework/platform/internal/status"
)

func archFixture() ArchInput {
	inv := &infraview.Inventory{Provider: "aws", Account: "1234", Resources: []infraview.Resource{
		{Provider: "aws", Kind: infraview.KindDNSZone, ID: "Z1", Name: "carloku.com",
			Attrs: map[string]string{"record_count": "12"}},
		{Provider: "aws", Kind: infraview.KindDNSRecord, ID: "r1", Name: "console.carloku.com",
			Attrs: map[string]string{"zone": "carloku.com", "type": "A", "value": "52.0.0.1"}},
		{Provider: "aws", Kind: infraview.KindIP, ID: "eipalloc-1",
			Attrs: map[string]string{"address": "52.0.0.1", "attached_to": "i-123"}},
		{Provider: "aws", Kind: infraview.KindMachine, ID: "i-123", Name: "carlos-eu-west-1", Region: "eu-west-1",
			Attrs: map[string]string{"type": "t4g.medium", "state": "running", "public_ip": "52.0.0.1"}},
		{Provider: "aws", Kind: infraview.KindDisk, ID: "vol-1",
			Attrs: map[string]string{"size_gb": "40", "attached_to": "i-123"}},
		{Provider: "aws", Kind: infraview.KindMachine, ID: "i-999", Name: "mystery-box", Region: "eu-west-1",
			Attrs: map[string]string{"type": "t4g.nano", "state": "running"}},
		{Provider: "aws", Kind: infraview.KindBucket, ID: "carlos-data-ie", Name: "carlos-data-ie"},
		{Provider: "aws", Kind: infraview.KindOther, NativeType: "sqs", ID: "arn:aws:sqs:eu-west-1:1234:q", Name: "q"},
	}}
	return ArchInput{
		Inv: inv,
		Fleet: &status.Fleet{Boxes: []status.FleetBox{
			{Label: "carlos-eu-west-1", Region: "eu-west-1"},
			{Label: "pi-dublin", Region: "home"},
		}},
		Hosts: []HostRow{
			{Label: "carlos-eu-west-1", Region: "eu-west-1", Metrics: &status.BoxMetrics{
				Release:       "abc1234",
				RouteVersions: map[string]string{"console.carloku.com": "abc1234"},
			}},
			{Label: "pi-dublin", Region: "home", Metrics: &status.BoxMetrics{}},
		},
		Routes: []registry.Route{
			{Host: "console.carloku.com", App: "console", Kind: "instance", Status: "ready", Version: "abc1234"},
		},
		Rates: economics.RateCard{Boxes: map[string]economics.BoxSpec{
			"":                 {HourUSD: 0.02, Cores: 2, MemGB: 4, EBSGB: 30},
			"carlos-eu-west-1": {HourUSD: 0.0336, Cores: 2, MemGB: 4, EBSGB: 30},
		}, EBSGBMonthUSD: 0.088},
	}
}

// The join that makes the picture true: a DNS record's value is an EIP's
// address, that EIP is attached to a machine, and that machine's Name tag
// is a reporting box's label.
func TestArchJoinsDNSThroughIPToBox(t *testing.T) {
	v := BuildArch(archFixture())

	var box *BoxCard
	for i := range v.Boxes {
		if v.Boxes[i].Label == "carlos-eu-west-1" {
			box = &v.Boxes[i]
		}
	}
	if box == nil {
		t.Fatal("no card for carlos-eu-west-1")
	}
	if box.CloudID != "i-123" {
		t.Errorf("CloudID = %q, want i-123 (Name tag matched to box label)", box.CloudID)
	}
	if box.InstanceType != "t4g.medium" {
		t.Errorf("InstanceType = %q", box.InstanceType)
	}
	if box.DiscoveredDiskGB != 40 {
		t.Errorf("DiscoveredDiskGB = %v, want 40", box.DiscoveredDiskGB)
	}
	if len(box.Apps) != 1 || box.Apps[0] != "console" {
		t.Errorf("Apps = %v, want [console]", box.Apps)
	}
	if !hasEdge(v, "record:r1", "ip:eipalloc-1") || !hasEdge(v, "ip:eipalloc-1", "box:carlos-eu-west-1") {
		t.Errorf("missing the DNS -> IP -> box chain in %+v", v.Edges)
	}
}

func hasEdge(v ArchView, from, to string) bool {
	for _, e := range v.Edges {
		if e.From == from && e.To == to {
			return true
		}
	}
	return false
}

// Unjoined in either direction is DRAWN, never hidden — the rule most of
// this page's value rests on.
func TestArchShowsUnjoinedBothWays(t *testing.T) {
	v := BuildArch(archFixture())

	var found bool
	for _, n := range v.Nodes {
		if n.ID == "machine:i-999" {
			found = true
			if n.Subtitle == "" {
				t.Error("an unclaimed machine must say so")
			}
		}
	}
	if !found {
		t.Error("a machine no box reports must still be drawn (it is drift)")
	}

	var pi *BoxCard
	for i := range v.Boxes {
		if v.Boxes[i].Label == "pi-dublin" {
			pi = &v.Boxes[i]
		}
	}
	if pi == nil {
		t.Fatal("a box with no cloud machine must still get a card — it is a BYO box, not an error")
	}
	if pi.CloudID != "" {
		t.Errorf("pi CloudID = %q, want empty", pi.CloudID)
	}
}

// Cost is the rate card's, so this page and /fleet/economics can never
// disagree; drift is flagged, not silently priced away.
func TestArchCostAndDrift(t *testing.T) {
	in := archFixture()
	v := BuildArch(in)
	var box BoxCard
	for _, b := range v.Boxes {
		if b.Label == "carlos-eu-west-1" {
			box = b
		}
	}
	want := 0.0336*economics.HoursPerMonth + 30*0.088
	if diff := box.USDMonth - want; diff > 0.001 || diff < -0.001 {
		t.Errorf("USDMonth = %v, want %v (rate card, not a second cost model)", box.USDMonth, want)
	}
	// The card says 30 GB of EBS; the estate has 40. That is drift.
	if len(box.Drift) == 0 {
		t.Error("a 30 GB rate-card entry against a 40 GB discovered disk must raise drift")
	}

	// A reporting box with no rate-card entry of its own is also drift:
	// it is being priced by the default spec, which is a guess.
	var pi BoxCard
	for _, b := range v.Boxes {
		if b.Label == "pi-dublin" {
			pi = b
		}
	}
	if len(pi.Drift) == 0 {
		t.Error("a box priced by the default spec must say so")
	}
}

// Resources the graph has no opinion about must still reach the page.
func TestArchGroupsCarryOtherResources(t *testing.T) {
	v := BuildArch(archFixture())
	for _, g := range v.Groups {
		for _, r := range g.Rows {
			if r.NativeType == "sqs" {
				return
			}
		}
	}
	t.Fatal("an unmapped resource must appear in the resource groups")
}

// No inventory at all: the CARLOS half still renders and Missing says which
// of the three states applies.
func TestArchWithoutInventory(t *testing.T) {
	in := archFixture()
	in.Inv = nil
	v := BuildArch(in)
	if v.Missing == "" {
		t.Error("a nil inventory must set Missing")
	}
	if len(v.Boxes) != 2 {
		t.Errorf("boxes = %d, want the CARLOS half to render regardless", len(v.Boxes))
	}
}
  • [ ] Step 2: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run TestArch -count=1 Expected: FAIL — undefined: BuildArch.

  • [ ] Step 3: Write archgraph.go

Implement BuildArch in this order, each step a small named helper so the file stays readable:

  1. machineFor(inv, label) — case-insensitive match of machine.Name against the box label; returns *infraview.Resource.
  2. diskGBFor(inv, machineID) — sum of disk.size_gb where attached_to == machine id.
  3. appsFor(host HostRow, routes []registry.Route) — the box's Metrics.RouteVersions keys looked up in routes by Host; distinct app names, sorted.
  4. boxCost and driftFor, in full — these two carry the page's money claims, so they are given rather than described:
// boxCost prices one box the way /fleet/economics prices it and no other
// way: the rate card's own spec for that label, falling back to the ""
// default exactly as economics.AllocateBox does. explicit is false when the
// fallback was used, which is itself worth telling the operator — a box
// priced by the default spec is priced by a guess.
func boxCost(rc economics.RateCard, label string) (usd float64, spec economics.BoxSpec, explicit bool) {
	spec, explicit = rc.Boxes[label]
	if !explicit {
		spec = rc.Boxes[""]
	}
	usd = spec.HourUSD*economics.HoursPerMonth + spec.EBSGB*rc.EBSGBMonthUSD
	return usd, spec, explicit
}

// driftFor names every way the rate card and the estate disagree about this
// box. Two checks only — the two this page can make honestly. Comparing the
// instance TYPE against the card's cores/memory would need a type->shape
// table, which is a second source of truth and out of scope (spec §10).
func driftFor(card BoxCard, spec economics.BoxSpec, explicit bool) []string {
	var out []string
	if !explicit {
		out = append(out, "priced by the default spec — no rate-card entry for this box")
	}
	if card.DiscoveredDiskGB > 0 && spec.EBSGB > 0 {
		if d := card.DiscoveredDiskGB - spec.EBSGB; d >= 1 || d <= -1 {
			out = append(out, fmt.Sprintf("rate card says %.0f GB of EBS, the estate has %.0f GB",
				spec.EBSGB, card.DiscoveredDiskGB))
		}
	}
	return out
}
  1. Node and edge construction, ids namespaced by kind (zone:Z1, record:r1, ip:eipalloc-1, box:<label>, machine:<id> for an unclaimed machine, apps:<label>, bucket:<name>): - zone → record for each record whose zone attr matches. - record → ip via inv.Find(KindIP, "address", record.Attr("value")). - ip → box when the IP's attached_to machine is a box's CloudID; ip → machine:<id> when it is not. - record → external:<value> when the value matches no IP and no machine — an arrow leaving the diagram, per spec §6.3. - box → apps:<label> carrying the instance counts as the node subtitle. - box → bucket:<deployment bucket> for every reporting box (the one bucket relationship the console can state as fact). - alarm → box when the alarm's target attr is that box's CloudID.
  2. Groups — compute (machine, disk), network (ip, firewall, network), storage (bucket), DNS (dns_zone, dns_record), identity, alarms, certificates, other. Each row's Detail is the resource's own attrs rendered k=v, sorted by key, so an attribute no one wrote code for still appears.
  3. Missing, TakenAt, Account, Partial copied from the inventory (with Missing set by the caller when Inv == nil; BuildArch sets "never" when handed nil and no reason).

The counts on the apps node come from the box's status.FleetBox.Routes (Static/Resident/Hibernating, and the *int Awake/Asleep rendered as "unknown" when nil).

  • [ ] Step 4: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run TestArch -count=1 -v Expected: PASS — five tests.

  • [ ] Step 5: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
gofmt -l internal/console
git add internal/console/archgraph.go internal/console/archgraph_test.go
git commit -m "console: architecture graph joins, box cards, rate-card cost and drift

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 6: Deterministic layout and SVG

Files:

  • Create: internal/console/archlayout.go, internal/console/archlayout_test.go
  • Modify: internal/console/archgraph.go (call layout at the end of BuildArch)

Interfaces:

  • Consumes: ArchView, Node, Edge (Task 5).
  • Produces: layout(v *ArchView) (sets every Node.X/Y/W/H and ArchView.Width/Height), RenderArchSVG(v ArchView) template.HTML.
  • [ ] Step 1: Write the failing test
package console

import (
	"strings"
	"testing"
)

// The same fleet must draw the same picture every time. A diagram that
// reshuffles between refreshes cannot be read for change, which is most of
// what an operator opens it for.
func TestLayoutIsDeterministic(t *testing.T) {
	a := RenderArchSVG(BuildArch(archFixture()))
	b := RenderArchSVG(BuildArch(archFixture()))
	if a != b {
		t.Fatal("two builds of the same input produced different SVG")
	}
}

// Layer order is the story the page tells: DNS enters at the left, routers
// and boxes in the middle, apps at the right.
func TestLayoutOrdersLayers(t *testing.T) {
	v := BuildArch(archFixture())
	x := map[string]int{}
	for _, n := range v.Nodes {
		x[n.ID] = n.X
	}
	if !(x["zone:Z1"] < x["record:r1"] && x["record:r1"] < x["ip:eipalloc-1"] &&
		x["ip:eipalloc-1"] < x["box:carlos-eu-west-1"] && x["box:carlos-eu-west-1"] < x["apps:carlos-eu-west-1"]) {
		t.Fatalf("layers out of order: %+v", x)
	}
}

// No node may overlap another: the layout is the only thing standing
// between a growing fleet and an unreadable page.
func TestLayoutDoesNotOverlap(t *testing.T) {
	v := BuildArch(archFixture())
	for i := range v.Nodes {
		for j := i + 1; j < len(v.Nodes); j++ {
			a, b := v.Nodes[i], v.Nodes[j]
			if a.X < b.X+b.W && b.X < a.X+a.W && a.Y < b.Y+b.H && b.Y < a.Y+a.H {
				t.Fatalf("%s overlaps %s", a.ID, b.ID)
			}
		}
	}
}

// The SVG must scale without script and must not carry raw user strings
// unescaped.
func TestSVGIsSelfContainedAndEscaped(t *testing.T) {
	in := archFixture()
	in.Fleet.Boxes[0].Label = `evil"><script>alert(1)</script>`
	in.Hosts[0].Label = in.Fleet.Boxes[0].Label
	out := string(RenderArchSVG(BuildArch(in)))
	if !strings.Contains(out, "viewBox=") {
		t.Error("SVG must carry a viewBox so it scales with no JS")
	}
	if strings.Contains(out, "<script") {
		t.Error("a box label must never reach the SVG unescaped")
	}
}
  • [ ] Step 2: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run TestLayout -count=1 Expected: FAIL — undefined: RenderArchSVG.

  • [ ] Step 3: Write archlayout.go
// Layout and SVG emission for /fleet/architecture. Deterministic by
// construction: layer by kind, order within a layer by one barycentre sweep
// each way, then a stable sort by node id. The same fleet must produce a
// byte-identical picture on every refresh — a diagram that reshuffles
// cannot be read for change.
//
// No JavaScript and no layout library: the console vendors its icons and
// loads nothing from a CDN, and a server-computed layout is the only way to
// keep that promise while still drawing a graph.
package console

import (
	"fmt"
	"html/template"
	"sort"
	"strings"
)

// Layer assignment by node kind — the story the page tells, left to right.
var archLayer = map[string]int{
	"zone": 0, "record": 1, "ip": 2, "router": 3,
	"box": 4, "machine": 4, "apps": 5, "bucket": 6,
}

const (
	nodeW    = 190
	nodeH    = 64
	gapX     = 90
	gapY     = 26
	padding  = 24
	satelliteKinds = "alarm identity firewall network disk certificate other"
)

func layout(v *ArchView) {
	// Satellites (alarms, identities, firewalls…) are listed in the cards
	// below the diagram rather than drawn: a graph that draws every IAM role
	// is a hairball, and the resource groups already carry them in full.
	kept := v.Nodes[:0]
	for _, n := range v.Nodes {
		if strings.Contains(satelliteKinds, n.Kind) {
			continue
		}
		kept = append(kept, n)
	}
	v.Nodes = kept

	byLayer := map[int][]int{}
	for i := range v.Nodes {
		l := archLayer[v.Nodes[i].Kind]
		byLayer[l] = append(byLayer[l], i)
	}
	// Stable order first, so the barycentre sweeps start from a fixed
	// arrangement and cannot depend on map iteration order anywhere.
	for l := range byLayer {
		idx := byLayer[l]
		sort.Slice(idx, func(a, b int) bool { return v.Nodes[idx[a]].ID < v.Nodes[idx[b]].ID })
	}
	// One sweep forward and one back, ordering each layer by the mean row of
	// its neighbours in the adjacent layer — the standard crossing
	// reduction, and enough for a graph this shape. Ties break on node id so
	// the result cannot depend on map or slice iteration order anywhere.
	row := map[string]int{}
	for _, idx := range byLayer {
		for r, i := range idx {
			row[v.Nodes[i].ID] = r
		}
	}
	neighbours := func(id string, forward bool) []string {
		var out []string
		for _, e := range v.Edges {
			if forward && e.To == id {
				out = append(out, e.From)
			}
			if !forward && e.From == id {
				out = append(out, e.To)
			}
		}
		return out
	}
	sweep := func(forward bool) {
		layers := make([]int, 0, len(byLayer))
		for l := range byLayer {
			layers = append(layers, l)
		}
		sort.Ints(layers)
		if !forward {
			for i, j := 0, len(layers)-1; i < j; i, j = i+1, j-1 {
				layers[i], layers[j] = layers[j], layers[i]
			}
		}
		for _, l := range layers {
			idx := byLayer[l]
			bary := map[string]float64{}
			for _, i := range idx {
				id := v.Nodes[i].ID
				ns := neighbours(id, forward)
				if len(ns) == 0 {
					bary[id] = float64(row[id])
					continue
				}
				sum := 0
				for _, n := range ns {
					sum += row[n]
				}
				bary[id] = float64(sum) / float64(len(ns))
			}
			sort.SliceStable(idx, func(a, b int) bool {
				ia, ib := v.Nodes[idx[a]].ID, v.Nodes[idx[b]].ID
				if bary[ia] != bary[ib] {
					return bary[ia] < bary[ib]
				}
				return ia < ib
			})
			for r, i := range idx {
				row[v.Nodes[i].ID] = r
			}
		}
	}
	sweep(true)
	sweep(false)

	maxRows := 0
	for l, idx := range byLayer {
		for row, i := range idx {
			v.Nodes[i].W, v.Nodes[i].H = nodeW, nodeH
			v.Nodes[i].X = padding + l*(nodeW+gapX)
			v.Nodes[i].Y = padding + row*(nodeH+gapY)
		}
		if len(idx) > maxRows {
			maxRows = len(idx)
		}
	}
	layers := 0
	for l := range byLayer {
		if l+1 > layers {
			layers = l + 1
		}
	}
	v.Width = padding*2 + layers*nodeW + (layers-1)*gapX
	v.Height = padding*2 + maxRows*nodeH + (maxRows-1)*gapY
}

// RenderArchSVG emits the diagram. Every string that came from a hostname,
// a tag or a label goes through template.HTMLEscapeString — these are
// operator-supplied values from a registry and a cloud account, and the one
// thing this page must never do is execute one of them.
func RenderArchSVG(v ArchView) template.HTML {
	var b strings.Builder
	fmt.Fprintf(&b, `<svg class="archsvg" viewBox="0 0 %d %d" role="img" aria-label="Deployment architecture">`, v.Width, v.Height)
	pos := map[string]Node{}
	for _, n := range v.Nodes {
		pos[n.ID] = n
	}
	// Edges first so nodes paint over them.
	for _, e := range v.Edges {
		from, okF := pos[e.From]
		to, okT := pos[e.To]
		if !okF || !okT {
			continue // a satellite edge whose node the layout dropped
		}
		x1, y1 := from.X+from.W, from.Y+from.H/2
		x2, y2 := to.X, to.Y+to.H/2
		mid := (x1 + x2) / 2
		fmt.Fprintf(&b, `<path class="archedge" d="M%d %d H%d V%d H%d" />`, x1, y1, mid, y2, x2)
	}
	for _, n := range v.Nodes {
		fmt.Fprintf(&b, `<g class="archnode archnode-%s">`, template.HTMLEscapeString(n.Kind))
		fmt.Fprintf(&b, `<rect x="%d" y="%d" width="%d" height="%d" rx="8" />`, n.X, n.Y, n.W, n.H)
		fmt.Fprintf(&b, `<text x="%d" y="%d" class="archtitle">%s</text>`,
			n.X+12, n.Y+24, template.HTMLEscapeString(n.Title))
		if n.Subtitle != "" {
			fmt.Fprintf(&b, `<text x="%d" y="%d" class="archsub">%s</text>`,
				n.X+12, n.Y+44, template.HTMLEscapeString(n.Subtitle))
		}
		b.WriteString(`</g>`)
	}
	b.WriteString(`</svg>`)
	return template.HTML(b.String())
}

Call layout(&v) as the last statement of BuildArch before it returns.

  • [ ] Step 4: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run "TestLayout|TestSVG|TestArch" -count=1 -v Expected: PASS.

  • [ ] Step 5: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
gofmt -l internal/console
git add internal/console/archlayout.go internal/console/archlayout_test.go internal/console/archgraph.go
git commit -m "console: deterministic architecture layout and inline SVG

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 7: The page, the gate and the refresh door

Files:

  • Create: internal/console/archpage.go, internal/console/archpage_test.go, internal/console/templates/architecture.html
  • Modify: internal/console/server.go (routes at :688-693, page struct), internal/console/templates/fleet.html (one link), internal/console/templates/layout.html (the page's CSS block)

Interfaces:

  • Consumes: BuildArch, ArchView, RenderArchSVG, loadInventory, FleetView, HostRows, openRegistry, the active scenario loader used by handleEconomics.
  • Produces: (*Server).handleArchitecture, (*Server).handleArchitectureRefresh, page.Arch *ArchView, page.ArchSVG template.HTML.
  • [ ] Step 1: Write the failing test
package console

import (
	"net/http"
	"net/url"
	"strings"
	"testing"
)

// TestFleetOperatorOnly's shape against /fleet/architecture: signed out,
// signed in but not an operator, and a revoked operator all get the exact
// 404 an unrouted path gets — byte-identical, so the route's existence is
// not probeable.
func TestArchitectureOperatorOnly(t *testing.T) {
	fx := newConsoleFixture(t)

	out := newBrowser(t)
	ghostStatus, _, ghost := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
	if ghostStatus != http.StatusNotFound {
		t.Fatalf("sanity: missing route = %d", ghostStatus)
	}
	if st, _, body := get(t, out, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost {
		t.Fatalf("signed-out = %d %q, want the same 404 as a missing route", st, body)
	}
	owner := signInPaul(t, fx)
	if st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost {
		t.Fatalf("non-operator = %d %q, want the same 404", st, body)
	}
	if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
		t.Fatal(err)
	}
	st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture")
	if st != 200 || !strings.Contains(body, "archsvg") {
		t.Fatalf("operator view = %d, want 200 with the diagram:\n%s", st, body)
	}
	if err := fx.st.SetOperator("paul@keymail.dev", false); err != nil {
		t.Fatal(err)
	}
	if st, _, body := get(t, owner, fx.ts.URL+"/fleet/architecture"); st != http.StatusNotFound || body != ghost {
		t.Fatalf("revoked = %d %q, want the same 404", st, body)
	}
}

// With no inventory ever taken, the page still renders and says WHICH of
// the three states it is in — never a confident "no resources".
func TestArchitecturePageWithoutInventory(t *testing.T) {
	fx := newConsoleFixture(t)
	c := signInPaul(t, fx)
	if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
		t.Fatal(err)
	}
	st, _, body := get(t, c, fx.ts.URL+"/fleet/architecture")
	if st != 200 {
		t.Fatalf("= %d, want 200", st)
	}
	if !strings.Contains(body, "No inventory has been taken yet") {
		t.Errorf("page must name the never-taken state:\n%s", body)
	}
	if strings.Contains(body, "no resources") {
		t.Errorf("page must not claim an empty estate it never read")
	}
}

// The refresh door: operator-gated, CSRF-checked, POST-only.
func TestArchitectureRefreshDoor(t *testing.T) {
	fx := newConsoleFixture(t)
	c := signInPaul(t, fx)
	if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
		t.Fatal(err)
	}
	called := 0
	fx.srv.inventory = func(ctx context.Context) (infraview.Inventory, error) {
		called++
		return infraview.Inventory{Provider: "aws", TakenAt: time.Now().UTC()}, nil
	}

	// No CSRF token: refused, and the pass must not have run.
	st, _, _ := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{})
	if st != http.StatusForbidden {
		t.Fatalf("no-CSRF refresh = %d, want 403", st)
	}
	if called != 0 {
		t.Fatalf("a refused refresh must not run the pass")
	}

	// With the token: runs once and redirects back to the page.
	csrf := sessionToken(t, fx, c)
	st, loc, body := postForm(t, c, fx.ts.URL+"/fleet/architecture/refresh", url.Values{"csrf": {csrf}})
	if st != http.StatusSeeOther || loc != "/fleet/architecture" {
		t.Fatalf("refresh = %d -> %q, want 303 back to the page:\n%s", st, loc, body)
	}
	if called != 1 {
		t.Fatalf("pass ran %d times, want 1", called)
	}
}

Helpers used above already exist: sessionToken(t, fx, c) and postForm(t, c, url, values) (status int, location, body string) — see appdomains_test.go:27. The test file imports context, net/url, time and internal/infraview on top of the usual set.

  • [ ] Step 2: Run test to verify it fails

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -run TestArchitecture -count=1 Expected: FAIL — 404 on every request; the route is not registered.

  • [ ] Step 3: Write archpage.go
// GET /fleet/architecture — the operator's map of the deployment: how a
// request reaches a process, and everything the account is carrying that
// makes that possible.
//
// The gate is handleFleet's, verbatim (server.go): session -> IsOperator ->
// bare http.NotFound on EVERY failure including the store error. A styled
// refusal would confirm the route exists, and this page names hostnames,
// instance ids and the shape of the estate.
//
// Every read soft-fails on its own. The inventory, the fleet aggregate, the
// per-box snapshots, the registry and the rate card are five independent
// reads; any one of them having a bad moment costs its own section and a
// line in the footer, never the page.
package console

import (
	"log"
	"net/http"

	"github.com/carlosframework/platform/internal/economics"
)

func (s *Server) handleArchitecture(w http.ResponseWriter, r *http.Request) {
	addr, raw, ok := s.operatorOnly(w, r)
	if !ok {
		return
	}
	in := ArchInput{}
	inv, missing := s.loadInventory(r.Context())
	in.Inv = inv

	if f, err := FleetView(r.Context(), s.cfg.Releases); err != nil {
		log.Printf("console: architecture: fleet view: %v", err)
	} else {
		in.Fleet = f
	}
	if hosts, err := HostRows(r.Context(), s.cfg.Releases, time.Now()); err != nil {
		log.Printf("console: architecture: hosts: %v", err)
	} else {
		in.Hosts = hosts
	}
	if reg, err := openRegistry(s.cfg.Registry); err != nil {
		log.Printf("console: architecture: registry: %v", err)
	} else if routes, err := reg.List(); err != nil {
		log.Printf("console: architecture: routes: %v", err)
	} else {
		in.Routes = routes
	}
	// The rate card comes from the SAME loader /fleet/economics uses
	// (economicsview.go:748), so the two pages cannot disagree about what a
	// box costs. Its warning is already surfaced on that page; here a bad
	// scenario simply means the zero rate card, and every box then reports
	// drift — which is the honest answer, not a silent $0.
	sc, _ := s.loadActiveScenario(r.Context())
	in.Rates = sc.RateCard

	v := BuildArch(in)
	if missing != "" {
		v.Missing = missing
	}
	p := page{Title: "Architecture", Address: addr, CSRF: raw, Arch: &v, ArchSVG: RenderArchSVG(v)}
	s.render(w, http.StatusOK, "architecture", p)
}

// handleArchitectureRefresh runs one inventory now. Single-flighted
// (infraRefreshing): a leaned-on button must not stack passes against a
// rate-limited API. It deliberately does NOT clear the auth latch — a
// deployment without the IAM grant gets the same honest answer however many
// times the button is pressed.
func (s *Server) handleArchitectureRefresh(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.NotFound(w, r)
		return
	}
	_, raw, ok := s.operatorOnly(w, r)
	if !ok {
		return
	}
	if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
		s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
		return
	}
	if !s.infraRefreshing.CompareAndSwap(false, true) {
		s.setFlash(w, "err", "An inventory is already running.")
		http.Redirect(w, r, "/fleet/architecture", http.StatusSeeOther)
		return
	}
	defer s.infraRefreshing.Store(false)

	if err := s.infraPass(r.Context()); err != nil {
		log.Printf("console: architecture refresh: %v", err)
		s.setFlash(w, "err", "The inventory could not be refreshed.")
	} else {
		s.setFlash(w, "ok", "Inventory refreshed.")
	}
	http.Redirect(w, r, "/fleet/architecture", http.StatusSeeOther)
}

Extract operatorOnly(w, r) (addr, raw string, ok bool) from handleFleet — the four-line gate, unchanged in behaviour — and have handleFleet, handleFleetMetrics, handleEconomics and both new handlers call it, so there is exactly one copy of the shape the tests pin. Extracting it must not change a single response: run the existing TestFleetOperatorOnly and TestEconomicsOperatorOnly before and after.

s.setFlash(w, kind, msg) takes "ok" or "err" as its kind (appdomains.go:57).

  • [ ] Step 4: Register the routes

In internal/console/server.go beside line 690:

	s.mux.HandleFunc("GET /fleet/architecture", s.handleArchitecture)
	s.mux.HandleFunc("POST /fleet/architecture/refresh", s.handleArchitectureRefresh)

And on the page struct, beside Metrics:

	// /fleet/architecture (archgraph.go): the deployment's map. Arch is nil
	// only on paths this handler never takes; ArchSVG is pre-rendered in Go
	// rather than assembled in the template because the layout has to be
	// deterministic and testable as a whole.
	Arch    *ArchView
	ArchSVG template.HTML
  • [ ] Step 5: Write the template

internal/console/templates/architecture.html:

{{define "architecture"}}{{template "top" .}}
<h1>Architecture</h1>
{{with .Arch}}
<p class="small">
  {{if .Missing}}
    {{if eq .Missing "never"}}No inventory has been taken yet — the first pass runs within the hour.
    {{else if eq .Missing "ungranted"}}This deployment has no infrastructure read grant, so the cloud half of this page is unavailable. The boxes below are what CARLOS itself reports.
    {{else}}The inventory snapshot could not be read.{{end}}
  {{else}}
    Estate {{.Account}} · inventoried {{fmtTime .TakenAt}}
  {{end}}
</p>

<div class="archwrap">{{$.ArchSVG}}</div>

<h2>Boxes</h2>
{{range .Boxes}}
<section class="card">
  <h3>{{.Label}}{{if .Region}} <span class="small">{{.Region}}</span>{{end}}</h3>
  <p class="small">
    {{if .InstanceType}}{{.InstanceType}} · {{end}}
    {{if .MemGB}}{{.MemGB}} GB · {{end}}
    {{if .DiscoveredDiskGB}}{{.DiscoveredDiskGB}} GB disk · {{end}}
    {{if .USDMonth}}${{printf "%.0f" .USDMonth}}/mo{{end}}
    {{if not .CloudID}} · no cloud machine (bring-your-own box){{end}}
  </p>
  <p class="small">Apps: {{if .Apps}}{{range .Apps}}<code>{{.}}</code> {{end}}{{else}}none{{end}}</p>
  <p class="small">
    static {{.Static}} · resident {{.Resident}} · hibernating {{.Hibernating}} ·
    awake {{intOrDash .Awake}} · asleep {{intOrDash .Asleep}}
  </p>
  {{range .Drift}}<p class="small chip-warn">{{.}}</p>{{end}}
</section>
{{end}}

{{range .Groups}}
<h2>{{.Title}}</h2>
<div class="tablewrap"><table>
  <tr><th>Name</th><th>Type</th><th>Region</th><th>Detail</th></tr>
  {{range .Rows}}<tr><td>{{.Name}}</td><td><code>{{.NativeType}}</code></td><td>{{.Region}}</td><td class="small">{{.Detail}}</td></tr>{{end}}
</table></div>
{{end}}

{{if .Partial}}
<h2>Reads that failed</h2>
<ul class="small">{{range .Partial}}<li>{{.}}</li>{{end}}</ul>
{{end}}
{{end}}

<form method="post" action="/fleet/architecture/refresh">
  <input type="hidden" name="csrf" value="{{.CSRF}}">
  <button type="submit">Refresh inventory</button>
</form>
{{template "bottom" .}}{{end}}

(Check the closing template name the other operator pages use — fleet.html's last line — and match it.)

Add to layout.html's style block, beside the other page-specific rules:

  /* /fleet/architecture: the diagram. Colours come from the same tokens as
     the rest of the console so it themes light and dark with no second
     palette to keep in sync. */
  .archwrap { overflow-x: auto; margin: 1rem 0 2rem; }
  .archsvg { width: 100%; min-width: 720px; height: auto; }
  .archsvg rect { fill: var(--accent-soft); stroke: var(--rule); }
  .archsvg .archnode-box rect { stroke: var(--accent); }
  .archsvg .archtitle { fill: var(--ink); font: 600 13px var(--sans); }
  .archsvg .archsub { fill: var(--muted); font: 400 11px var(--sans); }
  .archsvg .archedge { fill: none; stroke: var(--rule); stroke-width: 1.5; }
  • [ ] Step 6: Link it from the fleet page

At the top of internal/console/templates/fleet.html, after the <h1>:

<p class="small"><a href="/fleet/architecture">See the architecture →</a></p>
  • [ ] Step 7: Run tests to verify they pass

Run: GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./internal/console/ -count=1 Expected: PASS — the three new tests and every existing console test.

  • [ ] Step 8: Gate and commit
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
GOOS=darwin GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
gofmt -l internal/console
git add internal/console cmd/console
git commit -m "console: /fleet/architecture page, gate and refresh door

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 8: The IAM grant and the deploy note

Files:

  • Modify: infra/modules/region-host/main.tf (the box role's aws_iam_role_policy)
  • Modify: docs/superpowers/specs/2026-08-19-fleet-architecture-diagram-design.md (mark the grant's status)

Interfaces:

  • Consumes: nothing in Go.
  • Produces: the read-only inventory statement the pass needs.
  • [ ] Step 1: Read the existing policy

Run: sed -n '90,160p' infra/modules/region-host/main.tf Note the existing aws_iam_role_policy document's shape and its Statement list — the new statement is appended to it, in its style.

  • [ ] Step 2: Add the statement
      # Infrastructure inventory (2026-08-19-fleet-architecture-diagram-design.md
      # §8): what the console's hourly infra pass needs to draw
      # /fleet/architecture. Read-only in the strictest sense — every action
      # here is a List/Describe/Get of a resource's SHAPE. No write action,
      # no s3:GetObject, and deliberately no iam:GetPolicyVersion: the page
      # shows that a role exists and how many policies are attached to it,
      # never what those policies say.
      {
        Effect = "Allow"
        Action = [
          "ec2:DescribeInstances",
          "ec2:DescribeVolumes",
          "ec2:DescribeAddresses",
          "ec2:DescribeSecurityGroups",
          "ec2:DescribeVpcs",
          "ec2:DescribeSubnets",
          "ec2:DescribeRegions",
          "tag:GetResources",
          "s3:ListAllMyBuckets",
          "s3:GetBucketLocation",
          "s3:GetBucketVersioning",
          "s3:GetEncryptionConfiguration",
          "s3:GetBucketPublicAccessBlock",
          "route53:ListHostedZones",
          "route53:ListResourceRecordSets",
          "iam:ListRoles",
          "iam:ListUsers",
          "iam:ListAttachedRolePolicies",
          "cloudwatch:DescribeAlarms",
          "acm:ListCertificates",
          "sts:GetCallerIdentity",
        ]
        Resource = "*"
      },

(Resource = "*" is required: every action listed is an account-wide List/Describe that AWS does not scope to a resource ARN.)

  • [ ] Step 3: Validate the module

Run: cd infra/modules/region-host && tofu fmt -check && tofu validate (or terraform if that is what is installed; if neither is present, run tofu fmt -check only and note it). Expected: clean.

  • [ ] Step 4: Record the rollout status in the spec

Append to §8 of the spec:

**Status (2026-08-19):** the module statement is in this repo as of this
branch. The flagship box runs a role provisioned from pinfra, so the grant
reaches production only when the matching pinfra change is applied — until
then `/fleet/architecture` renders the CARLOS half and reports the estate
as ungranted, which is the designed state and not a fault.
  • [ ] Step 5: Commit
git add infra/modules/region-host/main.tf docs/superpowers/specs/2026-08-19-fleet-architecture-diagram-design.md
git commit -m "infra: read-only inventory grant for the architecture page

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"

---

Task 9: Branch finish

  • [ ] Step 1: Full gate, including the cross-compile CI runs
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go vet ./... && \
GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go test ./... -count=1 && \
GOOS=darwin GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod GOPROXY=off go build ./... && \
gofmt -l internal/infraview internal/awsinv internal/console cmd/console

Expected: every command clean, gofmt -l printing nothing.

  • [ ] Step 2: Confirm no AWS write reached the tree
grep -rnE "\b(Create|Put|Delete|Modify|Terminate|Run|Attach|Detach|Tag|Untag)[A-Z]" internal/awsinv/*.go | grep -v _test

Expected: no output. Any hit is a mutating SDK call that must not be here.

  • [ ] Step 3: Push the branch and open the PR
git push -u origin "$(git branch --show-current)"
gh pr create --title "Fleet architecture diagram: /fleet/architecture over a provider-neutral inventory" --body "$(cat <<'BODY'
Adds the operator page the spec describes: how a request reaches a process,
drawn over a live inventory of what the deployment's cloud account is
carrying, with each box's size, cost, apps and instance counts.

- `internal/infraview` — provider-neutral inventory shape (no SDK)
- `internal/awsinv` — the AWS implementation; the only SDK importer, read-only throughout
- `internal/console/infrapass.go` — hourly pass to `infra/inventory.json`, `cepull.go`'s latch posture
- `internal/console/archgraph.go` / `archlayout.go` — joins, cost/drift, deterministic SVG
- `GET /fleet/architecture` + `POST /fleet/architecture/refresh`, `handleFleet`'s gate
- `infra/modules/region-host` — the read-only grant (needs a matching pinfra apply to reach flagship)

Spec: `docs/superpowers/specs/2026-08-19-fleet-architecture-diagram-design.md`
Plan: `docs/superpowers/plans/2026-08-19-fleet-architecture-diagram.md`

Until the pinfra grant is applied the page renders the CARLOS half and
reports the estate as ungranted — the designed state, covered by a test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
BODY
)"
  • [ ] Step 4: Report what remains

State plainly in the handoff: the page has not been seen in a browser, and the AWS half has not run against a real account until the pinfra grant lands. Neither is a claim the tests can make.