Files
spinifex/docs/PACKAGE_BOUNDARY_MIGRATION.md
T
Josh Siviour b5879f44ff ADR-0001 package-boundary migration checkpoint (#1190)
* refactor: move telemetry file naming to contract

* docs: record telemetry contract move

* refactor: move aws xml response helpers

* docs: record aws xml helper move

* refactor: move aws tag projections to foundation

* docs: record aws tag projection move

* refactor: move account key to state foundation

* docs: record account key state move

* refactor: move host privilege escalation to runtime

* docs: record host privilege move

* chore: resolve lint findings from package-boundary moves

* refactor: move process supervision helpers to runtime host

* docs: record process supervision move

* refactor: move nbd endpoint helpers to compute runtime

* docs: record nbd endpoint move

* refactor: move image catalogue to operator

* docs: record image catalogue move

* refactor: move resource id generation to aws foundation

* docs: record resource id move

* refactor: move predastore role to runtime roles

* docs: point service manifest at acm domain

* docs: record predastore role move

* refactor: move northstar role to runtime roles

* docs: record northstar role move

* refactor: move awsgw role to runtime roles

* docs: record awsgw role move

* refactor: move error payload helpers to aws errors

* docs: record error payload move

* refactor: move account identifiers to aws identifiers

* docs: record account identifier move

* refactor: move NATS transport to messaging foundation

* docs: record NATS transport move

* refactor: move system instance contract to EC2 domain

* docs: record system instance move

* refactor: move system VPC builder to network domain

* docs: record system VPC move

* refactor: move instance metadata service to EC2 domain

* docs: record guest metadata move and dependency-rule debt

* refactor: move DNS control plane to DNS domain

* docs: record DNS domain move

* refactor: move base zone bootstrap to DNS domain

* docs: record base zone bootstrap move

* refactor: replace SystemAccountID with GlobalAccountID

* docs: record SystemAccountID removal

* refactor: extract CA key-pair loader to foundation pki

* docs: record foundation pki extraction

* refactor: move tenant CA paths to ACM domain

* docs: record tenant CA path move

* refactor: move IPsec certificate handling to network host

* docs: record IPsec certificate move

* refactor: move Ochre serving lifecycle to the Ochre domain

handlers/bedrock becomes domains/ochre. Ochre is the platform service;
Bedrock names only its AWS API surface. Persisted and observable
identifiers keep their bedrock names.

* docs: record Ochre domain move

* refactor: move Ochre vector store to the Ochre domain

handlers/ochrevector becomes domains/ochre/vector. Callers import it as
ochrevector because vector is a common local name in the gateway.

* docs: record Ochre vector store move

* refactor: move Hugging Face Hub client to the Ochre domain

gateway/bedrock/hfhub becomes domains/ochre/hfhub. Its only caller is
the spx admin ochre weights pull.

* docs: record Hugging Face Hub client move and Ochre gateway debt

* refactor: move EC2 tag mutation helpers to the EC2 domain

utils/tagmirror.go becomes domains/ec2/tagmirror. It is EC2-wide tag
mutation behaviour, and the tags resource package cannot host it
without a resource-to-resource import cycle.

* docs: record EC2 tag mutation move and migrate split finding

* refactor: move network packages under the network domain

Move spinifex/network/* to spinifex/domains/network/* as one enclosing
move so the L0-L5 layer invariants keep classifying every package from
a single root. Package names and dependency direction are unchanged;
invariant scan roots, the RG-10 sudo allow-list path, CODEOWNERS and
the repo-root walk in the IMDS netns unit test follow the new path.

* docs: record network domain move

* refactor: move standalone EC2 resource packages to the EC2 domain

Move handlers/ec2/{account,key,launchtemplate,placementgroup,
spotinstance,volumestate} to domains/ec2/<resource>. Each package is
named after its directory and callers import it as ec2<resource>;
volumestate already carried its directory name.

* docs: record standalone EC2 resource moves

* refactor: move EC2 VPC networking packages to the EC2 domain

Move handlers/ec2/{vpc,eigw,eip,igw,natgw,routetable} to
domains/ec2/<resource>, each named after its directory and imported as
ec2<resource>. The reconcile drift-watch source scan now matches the
ec2<resource> bucket qualifiers; its rule is unchanged.

* docs: record EC2 VPC networking moves

* refactor: move EC2 compute and storage packages to the EC2 domain

Move handlers/ec2/{instance,volume,snapshot,image,tags} to
domains/ec2/<resource>, each named after its directory and imported as
ec2<resource>, which empties handlers/ec2. The daemon's e2e suite
selection now lists the moved EC2 resource paths in place of
handlers/ec2, so EC2-only changes select the daemon suites again.

* docs: record EC2 compute and storage moves

* refactor: move ECR credential provider to the EKS agent tree

Move the kubelet exec credential provider implementation from
cmd/ecr-credential-provider to agents/eks/credentialprovider, exposing
one Main(in, out) entry point. The binary keeps its name and FIPS boot
import as a thin main. Moving out of cmd/ brings the code under gosec;
the one G101 hit on the kubelet API version constant is annotated.

* docs: record ECR credential provider move

* refactor: move EKS gateway publish helper to the EKS agent tree

Move the eks-gateway-publish implementation to agents/eks/gatewaypublish
behind Run(ctx, Config) error. The binary keeps flag definitions,
environment defaults, parsing, its log prefix and exit code; each former
fatal site returns an error with the same text. The retry wait now also
honours context cancellation, which the binary never triggers.

* docs: record EKS gateway publish move

* refactor: move EKS gateway fetch helper to the EKS agent tree

Move the eks-gateway-fetch implementation to agents/eks/gatewayfetch
behind Run(ctx, Config) error, writing its TSV to Config.Out. The binary
keeps flag definitions, environment defaults, parsing, its log prefix and
exit code; each former fatal site returns an error with the same text.

* docs: record EKS gateway fetch move

* refactor: move EKS webhook cert helper to the EKS agent tree

cmd/eks-webhook-cert keeps flag parsing, the log prefix and the exit;
agents/eks/webhookcert owns Config and Run. Output, error text, file
modes and exit codes are unchanged.

* docs: record EKS webhook cert move

* refactor: move EKS konnectivity cert helper to the EKS agent tree

cmd/eks-konnectivity-cert keeps flag parsing, the log prefix and the
exit; agents/eks/konnectivitycert owns Config and Run. Output, error
text, file modes and exit codes are unchanged.

* docs: record EKS konnectivity cert move

* refactor: move EKS token webhook to the EKS agent tree

cmd/eks-token-webhook keeps the -addr flag, environment-derived
config, logging setup and exits; agents/eks/tokenwebhook owns Config
and Run. The handlers/eks TokenReview result type stays as recorded
temporary debt.

* docs: record EKS token webhook move

* refactor: move ECS agent runtime and credentials packages to the ECS agent tree

* docs: record ECS agent subpackage move

* refactor: move ECS agent implementation to the ECS agent tree

cmd/ecs-agent keeps process lifecycle: signal context, the separate New
and Run calls, both failure log lines, the exit and the ldflags-set
build version, now passed in through Config.AgentVersion.
agents/ecs/agent owns Config, LoadConfig and the agent.

* docs: record ECS agent move

* refactor: move RDS guest agent to agents/rds/agent

* docs: record RDS guest agent move

* refactor: move installer packages to operator/installer

* docs: record installer move

* refactor: move spx command tree to operator/cli

Moving the command tree out of cmd/ brings it under errcheck and gosec.
Flag and config wiring now fails loudly instead of silently dropping a
binding, listing commands fail when they cannot write their table, and
the formation server's unauthenticated TLS is annotated as a known gap.

* docs: record spx command tree move

* chore: accept known vulnerabilities with an expiry

The vulnerability scan failed preflight on a containerd advisory that dev
already carries and that is deferred to a separate dependency update. The
gate now passes only reachable findings listed with an expiry date, fails
on any new one or on an expired acceptance, and warns when an acceptance
is no longer needed.

* refactor: move EC2 account action adapters to domains/ec2/awsapi

* refactor: move EC2 capacityreservation action adapters to domains/ec2/awsapi

* refactor: move EC2 key action adapters to domains/ec2/awsapi

* refactor: move EC2 launchtemplate action adapters to domains/ec2/awsapi

* refactor: move EC2 placementgroup action adapters to domains/ec2/awsapi

* docs: record EC2 standalone action adapter moves

* refactor: move EC2 zone action adapters to domains/ec2/awsapi

* refactor: move EC2 vpc action adapters to domains/ec2/awsapi

* refactor: move EC2 eigw action adapters to domains/ec2/awsapi

* refactor: move EC2 eip action adapters to domains/ec2/awsapi

* refactor: move EC2 igw action adapters to domains/ec2/awsapi

* refactor: move EC2 natgw action adapters to domains/ec2/awsapi

* refactor: move EC2 routetable action adapters to domains/ec2/awsapi

* docs: record EC2 VPC networking action adapter moves

* refactor: move EC2 image action adapters to domains/ec2/awsapi

* refactor: move EC2 snapshot action adapters to domains/ec2/awsapi

* refactor: move EC2 volume action adapters to domains/ec2/awsapi

* refactor: move EC2 tags action adapters to domains/ec2/awsapi

* docs: record EC2 storage and tag action adapter moves

* refactor: move EC2 resource authorization scopes to domains/ec2/awsapi

* refactor: move EC2 idempotency adapter to domains/ec2/awsapi

* refactor: move EC2 instance action adapters to domains/ec2/awsapi

* refactor: move EC2 spotinstance action adapters to domains/ec2/awsapi

* docs: record EC2 instance action adapter moves

* refactor: move AWS REST-JSON router to ingress/aws/rest

* refactor: move SigV4 auth-failure rate limiter to ingress/aws/ratelimit

* docs: record AWS ingress helper moves and tooling conformance

* docs: correct historical lifecycle references

* test: characterize ECR control-plane dispatch before registration seam

* feat: add AWS ingress service registration contract

* docs: record ingress registration phases 0 and 1

* refactor: route registered AWS services through the ingress dispatch registry

The gateway now consumes ingress/aws/dispatch. A registered service is
accepted by the auth gate, resolves its own action label, renders errors in
its declared envelope and is dispatched with an authenticated Invocation
(account, region, policy authorisation and a lazy verified actor). Every
other service keeps the existing switch, now named as the legacy path.

The awsgw role wires an empty registry and refuses to start if a registered
name is also served by the legacy table. No service is registered yet, so
behaviour for every existing service is unchanged.

* docs: record ingress dispatch registry consumption

* refactor: register the ECR control plane from its domain

domains/ecr/awsapi now registers the "ecr" AWS JSON 1.1 surface with the
ingress dispatch registry: action resolution, resource scope, authorisation
through the invocation, the composed capabilities and the success writer all
live with the domain. The awsgw role is the only serving composition point;
the gateway no longer imports the ECR API package and ECR leaves the legacy
service table, JSON error table and dispatch switch. The OCI /v2 route is
unchanged.

Operation coverage reads ECR's declared inventory, which the registration
uses verbatim; coverage.json is byte-identical.

* docs: record ECR control-plane registration

* fix: compose the ECR control plane in the integration harness

The harness stopped wiring ECR's composed capabilities when the
authorization-token action was composed, so every integration test that
creates a repository or mints a token got ServerInternal. It now composes
ECR as the awsgw role does, sharing the OCI registry, meta store and token
issuer the /v2 data plane uses.

* docs: record ingress pipeline deferral

* refactor: extract AWS error envelope rendering to ingress

ingress/aws/envelope owns the EC2, IAM-style and S3 XML error envelopes, the
AWS JSON 1.1 error body and X-Amzn-Errortype value, the JSON-protocol header
detector and the two error content types. Each shape keeps its own builder.

The gateway error writers, ErrorHandler and the legacy per-service envelope
selection stay in gateway and call the new package; gateway/eks renders its
JSON errors through it directly. A 140-case status/header/body golden
comparison across services and error writers was identical before and after.

* docs: record error envelope extraction

* refactor: move AWS service selection into ingress dispatch

Selector answers served, action, error protocol, registered entry and
startup overlap. The gateway supplies its legacy tables through the
dispatch.Legacy interface; legacy dispatch and invocation building stay
in gateway.

* docs: record ingress service selection move

* test: characterize ACM control-plane dispatch before registration

Pins full-pipeline error envelopes, a successful action, target parsing
agreement between dispatch and telemetry, authorization before the NATS
call, 413 and 503 paths, throttle/trace/audit action and the inventory.

* refactor: register ACM control plane from its domain

domains/acm/awsapi registers the acm JSON 1.1 surface through the
ingress dispatch registry, wired by the awsgw role. The gateway drops
ACM_Request, the ACM action table and every legacy acm entry; the
import invariant now covers each registered domain.

* docs: record ACM registration

* docs: record ECS, EKS and RDS source inventory and first-resource ranking

* docs: record EKS control-plane secrets persisted in the cluster record

* chore: drop containerd advisory acceptance now that dev ships v2.4.1

* test: characterize RDS DB subnet group lifecycle before extraction

* docs: record RDS DB subnet group lifecycle contract

* refactor: extract the RDS DB subnet group lifecycle owner

domains/rds/subnetgroup owns the db-subnet-groups/{name} record, member
subnet validation and the delete in-use guard. handlers/rds keeps AWS
validation, projection and paging and binds two consumer-owned
capabilities: the owner's Dependants (instances naming the group) and the
instance side's subnetGroupPlacement. Stored shape and behaviour are
unchanged.

* docs: record RDS DB subnet group owner extraction

* docs: correct RDS subnet group tagging debt and conformance scope

* test: characterize ECS task definition lifecycle before extraction

* docs: record ECS task definition lifecycle contract

* refactor: extract ECS task definition owner into domains/ecs/taskdefinition

Move the task definition record, container types, keys, reference parsing,
ARN helpers and the register/deregister/resolve/list logic into a resource
owner package. Stored JSON field names and KV keys are unchanged, and
revision allocation keeps its existing non-atomic read-then-write.

The owner declares its own PortMapping and SystemControl persisted types so
it no longer depends on handlers/ecs/bus; the handler translates them onto
the assign payload. Task and service launches reach task definitions only
through a launch-side taskDefinitionResolver. AWS conversion, the guest
projection and tags stay in handlers/ecs, and gateway authz only switches to
the moved reference/ARN helpers.

List handlers/ecs and the new package under spinifex_daemon so ECS-only
changes select e2e-ecs.

* docs: record ECS task definition owner extraction

* test: characterize EKS access entry lifecycle before extraction

* docs: record EKS access entry lifecycle contract

* refactor: extract EKS access entry owner into domains/eks/access

Move the access entry record, associated policy and scope types, keys,
PrincipalARNHash, the policy catalogue and group mapping into a resource
owner package. Stored JSON field names and KV keys are unchanged.

Owner is the only writer: Create, SeedCreatorAdmin, Update, Associate,
Disassociate and Delete carry the existing logic verbatim, and the put,
compare-and-set and delete primitives are private. The handler keeps AWS
validation, the cluster existence check, error mapping and projection.
Token review still reads records through the exported Get, and the cluster
prefix purge still sweeps entry keys.

List handlers/eks and the new package under spinifex_daemon so EKS-only
changes select the EKS suites.

* docs: record EKS access entry owner extraction

* test: characterize RDS engine catalogue before extraction

* fix: return InvalidParameterValue for a missing RDS master password

ValidateMasterUserPassword built the empty-password error with a bare
errors.New carrying the code only in its text, which awserrors does not
resolve, so CreateDBInstance without MasterUserPassword returned HTTP 500
ServerInternal. Build it with awserrors.Errorf like the other branches so
the gateway returns 400 InvalidParameterValue, and cover the full gateway
to RDS service path with a regression test.

* docs: record RDS engine catalogue boundary and evidence

* refactor: extract RDS engine catalogue into domains/rds/engine

Move engine identity and validation, the per-engine parameter catalogues,
effective-parameter resolution, db.* instance classes and engine/orderable
selection into an engine package. Behaviour, error codes and ordering are
unchanged.

The package imports no EC2 package: instance-class memory comes from an
immutable Sizing value built from an engine-declared InstanceTypes
interface, backed by the fixed instance-type table in handlers/rds.
Effective parameters leave as an engine-owned Setting, converted to
Parameter in handlers/rds. AWS output building, paging, apply-method
mapping, storage limits and the gateway runnable check stay in the
adapter path.

The RDS agent declares its own EngineCatalog interface and types and
receives an engine-backed implementation from cmd/rds-agent, so agent
code does not import the catalogue. Cross-engine tests use the two real
engines instead of a registered fake.

* docs: record RDS engine catalogue owner move

* test: characterize RDS DB parameter group lifecycle before extraction

* docs: record RDS DB parameter group lifecycle contract

* refactor: extract RDS DB parameter group owner

Move the parameter group and override records, keys and listing into
domains/rds/parametergroup behind an Owner. Delete asks an owner-declared
Dependants interface; instance-side resolution reads through its own
parameterGroupValues interface. SetOverrides refuses an unknown group so
no override key is written without its group. Stored shape, keys, public
errors and propagation are unchanged.

* docs: record RDS DB parameter group owner extraction

* test: characterize EKS add-on lifecycle and guest wire

Pin the persisted record and manifest layout, create/update/delete and
status-report transitions, the ignored client token and report version,
the missing serviceAccountRoleArn PassRole check, and the exact internal
add-ons HTTP body and agent TSV output as they stand today.

* docs: record EKS add-on lifecycle contract

* fix: require iam:PassRole for EKS add-on service account role

CreateAddon and UpdateAddon now pass serviceAccountRoleArn through the
existing EKS PassedRoleARNs path, so the caller needs iam:PassRole on the
role for eks.amazonaws.com, matching CreateCluster and CreateNodegroup.

* docs: record EKS add-on PassRole check

* refactor: extract EKS add-on owner

Move the add-on catalogue, record and manifest keys, CAS, status
transitions and staging into domains/eks/addon behind an Owner that is
the sole writer. The handler keeps AWS validation, error mapping and
projection; the cluster reconciler delegates reports through the narrow
addonReports interface; node groups call EnsureGPUDevicePlugin. Stored
keys, JSON shape, wire types and lifecycle behaviour are unchanged.

* docs: record EKS add-on owner extraction

* refactor: extract EKS add-on guest wire contract

Move the add-on status subject, delivery phase, status report, staged
manifest and internal add-ons response into contracts/eks/v1 and use it
from the handler, gateway and gatewayfetch, replacing the agent's
duplicate. Wire bytes are unchanged, including the internal add-ons body
that the SDK renderer emits with Go field names; compatibility tests pin
the JSON, the guest printf form and the TSV.

* docs: record EKS add-on wire contract extraction

* docs: update EKS add-on inventory and remaining edges

* docs: draft EKS add-on lifecycle correction design for review

* chore: run contracts tests in the default test, coverage and race targets

* docs: apply review decisions to EKS add-on lifecycle design

* test: characterize EKS add-on v1 compatibility with added fields

Pin which owner writes drop unknown record and manifest fields, that
reads and deletes ignore or erase them, that one undecodable entry fails
a listing, and that v1 wire consumers ignore extra fields.

* docs: add EKS add-on executor, bundle, v1 compatibility and release inventories

* docs: draft EKS add-on v2 guest contract design for review

* docs: qualify historical architecture references

* docs: apply review decisions and ADR-0006 to EKS add-on v2 design

* fix(eks): bind internal report publishing to the caller's cluster

PublishInternal was authorised by IAM policy alone, with its resource
built from the body accountId and the URL cluster. The control-plane
role grants it on "*", so any control-plane VM session, any tenant
holding eks:*, or static system credentials could publish cluster
state, bootstrap and add-on reports for another cluster or account.

PublishInternal now passes the existing internal-route gate: the caller
must be a control-plane instance-role session that is a member of the
URL cluster in the body account. The gate and the handler decode the
body through one shared decoder, so the account checked is the account
the subject is built from. Every unauthorised target returns the same
AccessDenied, and invalid account or cluster names are treated as not
found. Valid publishes keep their subjects and payload bytes.

* fix(eks): bind webhook token review to the caller's cluster

WebhookTokenReview took its account from the request body and was
authorised by IAM policy alone, with the control-plane role granting it
on "*". A control-plane VM, any tenant holding eks:*, or static system
credentials could resolve a token against another cluster or account
and learn the Kubernetes groups it maps to there. Error responses also
distinguished absent accounts from absent clusters.

WebhookTokenReview now passes the same internal-route gate as
PublishInternal: a control-plane instance-role session that is a member
of the URL cluster in the body account. The gate and the handler share
one body decoder, each body-scoped internal route must declare its
reader, and every unauthorised target returns the same AccessDenied
without reaching STS verification. Same-cluster responses are unchanged.

* fix(eks): validate add-on serviceAccountRoleArn syntax

CreateAddon and UpdateAddon stored serviceAccountRoleArn without
checking it was an ARN, and the guest renders the value into the
add-on's manifests. A non-empty value must now be syntactically an IAM
role ARN (12-digit account, no region, role resource, IAM name and path
rules); anything else returns InvalidParameterException before any
record is written or manifest staged.

The syntax check is foundation/aws/arn.ValidateRoleARN. iam:PassRole at
the gateway remains the permission and existence check. Stored values
are not rewritten, and an UpdateAddon that omits the field still
succeeds on a record holding a legacy value.

* docs: record EKS internal-route and role-ARN security corrections

* docs: add EKS add-on release membership and activation case study

* fix: point UI build, CI and lint rules at moved package paths

* docs: record EKS add-on activation review decisions

* refactor: move quota admission to domains/admission/quota

Relocate the quota service, live counters and vCPU records from
handlers/quota to domains/admission/quota. Package renamed from
handlers_quota to quota; callers with a local quota identifier import it
as admissionquota. KV buckets, subjects and error codes are unchanged, and
the package's existing debt edges move with it unchanged.

* docs: reconcile package-boundary record for ADR-0001 close-out

Add the missing slice rows (the commits that dissolved spinifex/types, the
ECR repository projection, the stale-path fix and the quota move), correct
stale entries, record the dependency-rule breaks found in the import graph
with removal conditions, add the close-out table of remaining legacy paths
with their decided target owners, recount the mulga-side follow-ups, and
fix stale old-path references in docs and comments.

* test: move KV migration engine tests to foundation/state/migrate

The Registry and RunKV tests exercise foundation/state/migrate exclusively
but lived in spinifex/migrate's test file. Relocate them, with their NATS
helpers, to the package that owns the engine. No test name, assertion or
behaviour changes.

* refactor: move config and object-store upgrade workflow to operator/upgrade

Relocate spinifex/migrate, the operator-driven config-file and object-store
upgrade workflow behind spx admin upgrade, to operator/upgrade. Package
renamed to upgrade (imported as opupgrade where a local upgrade identifier
exists). It stays distinct from foundation/state/migrate, the generic KV
migration engine; no registry types, migration names, versions, file
formats or command names change.

* docs: record migrate move to operator/upgrade

* test: enforce ADR-0001 layering with a blocking ratchet

Add spinifex/architecture/layering, a test-only package that parses the
module's import graph, maps every production package to an ADR-0001
layer and checks each dependency rule as its own subtest. Existing
violations are allow-listed against their recorded debt; a new forbidden
edge, an unmapped package, a stale allowlist entry or a new package
under a frozen legacy root fails local preflight and CI.

Record the nine edges the test found missing from the migration record
(quota -> EC2 instance types, STS/Ochre/ELBv2 -> IAM, ELBv2
cross-domain, installer UI -> admin), name the importers of the utils
residuals and gateway/ec2_idempotency.go, and correct the handlers/iam
consumer list.

* refactor: drop the legacy handlers_ecr import alias

Import domains/ecr under its package name, or as ecrdomain where the
file also imports the AWS SDK ecr package, matching acmdomain. Also fix
a stale daemon file reference in the integration ECR daemon comment.

* chore: mark spf13/pflag as a direct dependency

operator/cli/wiring.go imports pflag directly. Same version; no upgrade.

* docs: record test-package-check and nilaway as unverified gates

* refactor: move the layering test to module-root architecture/

The ADR-0001 layering ratchet is repository governance tooling, not
Spinifex product code, so it sits beside tests/ rather than under the
spinifex/ product tree. make test, test-cover and test-race now include
./architecture/..., and the graph walk excludes architecture/.

* fix: update paths left stale by the package moves

The MariaDB image stamp test read the RDS engine from
handlers/rds/engine_mariadb.go, so test-images failed in CI; it now reads
domains/rds/engine/mariadb.go. Comments citing moved files now name
their new locations.

* refactor: move the default-VPC subject into contracts/ec2/v1

utils.SubjectEnsureDefaultVpc becomes ec2v1.EnsureDefaultVpcSubject, the
request/reply route between gateway account creation and the daemon.
Only the subject moves; the request and reply bodies are still anonymous
structs on each side and are recorded as such in the contract README.
The layering freeze now allows two utils files.

* docs: qualify the migrate split and record admin and Viperblock findings

The migrate row is a split, not an exact ADR-0001 map row, and needs a map
clarification. Correct the stale admin consumers, record the operator-only
admin files as movable, the vbwire prerequisite for the Viperblock adapter
and the capability decision encryption.go needs.

* refactor: move operator-only admin code to operator/admin

Config generation, certificate issuance, host discovery, telemetry,
email validation and image import/removal/orphan-volume tooling move
to operator/admin. Legacy admin keeps only the symbols with
non-operator consumers: default account identifiers, AWS credential
generators and system image promotion.

foundation/pki and the tenant ACM CA are unchanged. The readAMI
helper and the removal test fixtures are duplicated privately in
legacy admin rather than shared across the split. The now-stale
operator/installer/ui -> admin layering allowlist entry is removed.

* docs: record operator/admin split in package-boundary migration

* test: pin viperblockd fence, abandon and dirty-marker wire shapes

Characterization tests against the current vbwire package, before it
moves to contracts/viperblockd/legacy/v1: both subject forms for the
fenced and abandon routes, golden JSON for VolumeFencedEvent,
VolumeAbandonRequest, VolumeAbandonResponse and DirtyRecord, the dirty
bucket name, and the volume-key pattern shared by the lease and dirty
stores.

* refactor: move viperblockd fence, abandon and dirty-marker vocabulary to contracts

Move the fence and abandon subjects/DTOs, the dirty bucket name,
DirtyRecord and the shared volume-key pattern out of vbwire and into
contracts/viperblockd/legacy/v1, alongside the package's existing
mount/unmount/delete/sync/config-update routes. vbwire keeps only the
I/O helpers (BindDirtyBucket, ListUnsealedVolumes), now built on the
contract vocabulary instead of defining it.

Record the approved dirty-marker ownership boundary in the contract
README: the Viperblock adapter is the sole producer, viperblockd was
only its transitional custodian. Document the fence and abandon routes
and the dirty bucket in docs/service-interfaces.yaml.

* refactor: split the Viperblock EBS adapter out of services/viperblockd

Move the EBS provider adapter (volume lease, fence, dirty marker,
nbdkit recovery, provider handlers, encryption key loading) to
providers/ebs/viperblock. services/viperblockd keeps only process
lifecycle and the NATS subscription wiring that starts and stops it.

The dirty-bucket read path moves to providers/ebs/viperblock/dirty as
a read-only ListUnsealedVolumes capability; the adapter's write, clear
and purge logic stays unexported in the adapter package. Delete
services/viperblockd/vbwire now that its vocabulary lives in
contracts/viperblockd/legacy/v1 and its I/O helpers live in dirty.

Update operator/cli, the runtime service registry, qemunbdd's config
comment and the integration test harness to the new import paths, and
drop the now-stale architecture/layering allowlist and frozen-package
entries for the deleted package. Behaviour, persisted keys and NATS
subjects are unchanged.

* refactor: move the viperblockd role to runtime/roles/viperblockd

With the EBS adapter in providers/ebs/viperblock, services/viperblockd
holds only role supervision and the opt-in live data-path tests, so it
joins the other runtime roles. It was the last package under services/;
the frozen legacy list drops it.

* docs: record viperblockd contract, adapter and role moves

* test: characterise vpc.* network-realisation routes

Pin every vpc.* subject, JSON payload shape and the request/reply
{success,error} ack envelope exactly as they exist today, ahead of
introducing contracts/network/v1. Covers the 22 subjects registered
by domains/network/subscribers, the vpc.igw-attach/detach subjects
and InternetGatewayEvent already owned by contracts/ec2/v1, the
utils.AddNAT/PublishNATEvent client wire behaviour including the
delete-nat retry-on-ErrNoResponders budget, and the EC2-side NATEvent
and SGEvent/SGRule duplicates in domains/ec2/vpc.

Also adds the six vpc.* route-table and system-egress subjects that
were missing from service-interfaces.yaml despite being live
production routes (vpc.add/delete-igw-route, vpc.gate/ungate-subnet-
egress, vpc.add/delete-system-egress).

* feat: add contracts/network/v1 for vpc.* realisation routes

Declare the versioned wire vocabulary for the vpc.* control-plane-to-
network-realisation boundary: subjects, payload types, the shared
{success,error} ack envelope and the vpcd-workers queue group, one
file per route family. No NATS I/O, no consumer switched over yet;
the package coexists with the fragmented originals pinned in the
prior characterization commit until slice 3 migrates call sites.

* refactor: use contracts/network/v1 for vpc.* producers and consumers

Switch every vpc.* producer (domains/ec2/vpc, eip, instance, natgw,
routetable, igw, handlers/elbv2, handlers/eks, daemon, systemvpc) and the
network/subscribers consumer onto contracts/network/v1 subjects, event
types and the shared AckEnvelope. contracts/network/v1 becomes the sole
owner of vpc.igw-attach/detach, replacing the duplicate definitions in
contracts/ec2/v1.

Deletes the duplicated local subject constants and event structs:
subscribers' Topic* consts and event structs, vpc.NATEvent and SGEvent,
eip's natEvent, natgw's natGatewayEvent, and eks's systemEgressEvent.
domains/ec2/vpc.SGRule survives because EC2 persists it directly in
SecurityGroupRecord; contracts/network/v1.SecurityGroupRule was expanded
in slice 2 to match it field-for-field so wire bytes are unchanged.

No lifecycle or reliability changes: the NAT add barrier, delete
retry-on-ErrNoResponders, and the inconsistent per-call-site NAT
delivery modes (EIP AssociateAddress and DeleteNetworkInterface both stay
fire-and-forget with no ack) are preserved exactly, as are the golden
wire literals pinned in the characterization tests.

* refactor: replace utils/vpcd_event.go with a typed network projection client

Every vpc.* call site now goes through domains/network/projection's
operation-specific methods instead of utils.AddNAT, utils.PublishNATEvent
or utils.RequestEvent, so no caller passes a bare subject string. Each
method states today's delivery mode by name: AddNAT stays strict,
AddNATBestEffort and AnnounceNATAdd/AnnounceNATDelete keep the gaps those
call sites already had, and RemoveNAT keeps the ErrNoResponders retry.

utils/vpcd_event.go and its tests are deleted; the behaviour they pinned
(wire shape, ack envelope, retry-on-no-responders) moved into the
projection package's own golden and nat tests, unchanged. utils/ now
holds only encryption.go, which this does not touch.

* refactor: route the remaining fire-and-forget vpc.* producers through projection

CreateVPC/DeleteVPC, CreateSubnet/DeleteSubnet, AttachInternetGateway/
DetachInternetGateway, AddNATGateway/RemoveNATGateway, AddIGWRoute/
RemoveIGWRoute, GateSubnetEgress/UngateSubnetEgress and RemoveSystemEgress
were still publishing through natsmsg.PublishEvent or a raw nc.Publish
call with a bare subject string, left over from the mechanical slice 3
pass. Each now goes through its named projection.Client method instead,
so every vpc.* producer in the tree uses the typed client.

* refactor: move EBS encryption posture to domains/ec2/ebs/encryption and delete utils

The master-key loader was used only by EC2 image, instance and volume to
derive the AWS-visible Encrypted flag. It becomes ebsencryption.Enabled,
EC2's temporary reading of the provider's key-file setting; the cache,
empty-path meaning, error text and each caller's error handling are
unchanged (image still reports unencrypted when the key will not load).
Its successor is a providers/ebs capability reporting volume encryption
posture.

utils/ is now empty and deleted. The layering test drops utils and
services from the legacy roots, removes their allowlist entries and fails
if either retired root regains files. The S7 subject scan follows the
vpc.* literals from utils to contracts/network so its gap list stays
complete.

* docs: record the network contract, projection client and utils removal

* test: characterise instance-state persistence, recovery and generation handling

Pin the current local-file bytes and schema failure modes, the instance
bucket configuration, record key and wire form, the bucket version gate,
generation preservation across every KV writer and the VM conversion, and
what a returning node does for a missing, foreign, terminal or unreadable
canonical record. These describe current behaviour, not the ADR-0007 target.

* test: freeze direct instance-state consumers

Add an AST ratchet over production code that fails when a new file names
the daemon's instance-state bucket, record keys, local-file API, the raw
vm.InstanceRecord or its conversion, or a JetStreamManager instance-state
method. Today's 73 uses form a shrink-only allowlist keyed to the slice 0
inventory; stale entries fail, and a synthetic-source self-test proves the
matcher detects each kind of use and ignores declarations.

* docs: record ADR-0007 slice 0 instance-state inventory

Record the instance-state persistence formats, the nineteen direct
consumers by process and access kind, the characterised recovery and
schema outcomes, the generation-preservation matrix and the ratchet that
freezes the consumer set. Nothing conforms to ADR-0007 yet and vpcd stays
blocked.

* test: narrow the instance-state ratchet to instance-state access

Stop freezing the JetStreamManager type and constructor, which also serve
cluster state; flag only instance-state methods, bucket and key identifiers,
record codecs and local-state access. The cluster-state-only consumers in
daemon_mgmt_ip.go and operator/cli/cluster.go leave the allowlist (73 to 67).
The self-test now proves a cluster-state use is ignored and an instance-state
method call is caught. Record the owner's disposition of the KV-unavailable
restore result in the migration record.

* docs: record the narrowed instance-state ratchet commit

* docs: design the instance-state transition record and activation gate

* docs: record the instance-state transition-record design commit

* docs: record slice 1a activation-gate decisions

* feat: add instance transition-record codec with format marker

Adds DecodeTransitionRecord and TransitionRecord.Encode in the record's owner
package. A top-level format member selects the shape: absent or 1 decodes the
legacy record and is never given a marker on write; 2 also decodes the new
assignment and observation sections; an integer above 2 decodes its legacy
fields read-only and Encode refuses it; anything else is refused on decode.

Encode applies only the typed paths that differ from the decode-time snapshot
onto the input, so unknown members at every object level and unchanged known
fields keep their read form. A changed array is replaced whole. Decode refuses
malformed JSON, duplicate keys at any depth, keys that alias a known field by
case, trailing data and a non-object record, each with its own sentinel.

No production reader or writer uses the codec yet; the new types are not
part of vm.InstanceRecord, so every existing record encodes unchanged. The
codec file is registered as an owner file of the instance-state ratchet.

* docs: record slice 1b step 1 codec and awsgw rollout qualification

Records the codec added in e62f921d2 as design Section 10 (C1 to C7): its
surface, preservation at every object level, the changed-array limitation,
marker handling, the rejection classes and its capable-writers-only scope. C2
supersedes P1's top-level-only preservation for the codec. G19 records that
old read-only gateway consumers remain safe but DescribeInstanceStatus gains
unconfirmed/degraded visibility only after awsgw upgrades. The migration record
gains the commit row and states that no production path uses the codec yet.

* refactor: move the instance transition-record codec to domains/ec2/instance/state

The codec defines how the canonical persisted instance record evolves, with
its assignment and observation sections, which is EC2 instance ownership rather
than node-local VM supervision. It moves unchanged into its own package and
keeps wrapping the legacy vm.InstanceRecord alias and its spec and status
types, which stay in runtime/compute/vm for now.

The domain-to-runtime import is recorded as new dependency-rule debt with its
removal condition and allow-listed against that entry. The instance-state
ratchet now names the new file as an owner file; its allowlist is unchanged.

* fix: refuse array changes that would drop element extensions

A changed array is replaced whole by the writer's value, which silently lost
any member a newer writer had added inside its elements. Encode now inspects
the array as read: if any element carries a member its typed schema does not
know, at any depth, it returns ErrRecordExtensionConflict naming the array and
the first such member. Arrays without such members are still replaced whole,
unchanged arrays are still kept verbatim, and arrays inside unknown members are
never inspected.

* docs: record the codec move and the array extension-conflict rule

Records 8db9771fb and c617914a1. T1 and Section 10 now name
domains/ec2/instance/state as the eventual owner of the persisted instance
record and its evolution, with vm.InstanceRecord as the temporary legacy type
it wraps. C3 replaces the changed-array limitation with the refusal: a changed
array with element extensions fails with ErrRecordExtensionConflict and is never
silently dropped. P2 stays a conservative rule until finalisation.

* fix: refuse removals that would drop record extension fields

A capable writer may no longer remove or replace a known object, map entry or
array when its stored value carries members the writer's schema does not know.
The check that guarded changed arrays now runs wherever the merge drops or
overwrites a stored value instead of merging into it: a known member deleted or
set to null, a map entry removed or stored under a key the typed decode
rewrote, a value of another JSON kind, and a changed array. Encode returns
ErrRecordExtensionConflict naming the changed container and the first unknown
member. Removal stays allowed when the stored value has no extensions, and a
change merged member by member still keeps unknown members.

* docs: record the uniform extension-conflict rule

Adds design clause C8 for 0d7b4266d: a capable writer may not remove or
replace a known object, map entry or array whose stored value carries unknown
structured extension fields; removal without extensions stays allowed. C3 and
C5 point at it. The migration record gains the commit row, updated subsection
wording and a pointer-bump follow-up deferring make docs-portal to the Mulga
gitlink PR so the portal evaluates the proposed Spinifex revision.

* refactor: inject vpcd's IMDS instance-state readers from the instance state boundary

* docs: cite the vpcd reader injection commit in the migration record

* refactor: move vpcd to runtime/roles/vpcd

* docs: cite the vpcd move commit in the migration record

* refactor: move the ECS agent wire vocabulary to contracts/ecs/v1

Move handlers/ecs/bus to contracts/ecs/v1 (package ecsv1) with no legacy
shim. Subject strings, JSON fields and types are byte-identical; the old-agent
compatibility test moves unchanged and golden pins cover the remaining
message types. The README route table records that only service-reconcile
has a live producer today; the agent reuses these payload types through the
gateway actions. service-interfaces.yaml lists ecs.bus.* under the daemon.

* refactor: move system-image promotion to domains/ec2/image

Move images_promote.go and its tests from admin to domains/ec2/image:
SystemOwnerAlias, PromoteImageOpts/PromoteImageResult, PromoteSystemImage,
GetAMIMetadata and the private snapshot reader. Storage, ordering, errors and
promotion behaviour are unchanged; the daemon, gateway/spx and operator CLI
callers import ec2image. admin now holds only account IDs and key generators.

* docs: record the ECS contract and image-promotion moves and the remaining-roots matrix

* refactor: move the bootstrap account identifiers to foundation/aws/identifiers

DefaultAccountID and DefaultAccountName join GlobalAccountID as the fixed,
platform-reserved identity of the first human-facing bootstrap account.
Names and values are unchanged and stay fixed. Callers in daemon, gateway,
awsgw, accountteardown and the operator CLI use awsidentifiers; admin now
holds only the IAM credential generators.

* docs: cite the bootstrap account identifier move in the migration record

* refactor: inject the STS cluster JWKS lookup from handlers/eks

handlers/sts declares ClusterJWKSResolver and keeps JWKS decoding and
InvalidIdentityToken mapping; handlers/eks implements it as
OIDCJWKSReader beside the bucket and key layout helpers. awsgw and the
integration harness inject the reader. STS no longer imports any EKS
package, including tests, and the handlers/sts -> handlers/eks layering
exception is removed.

* docs: cite the STS JWKS resolver cut in the migration record

* refactor: move the AWS credential-format generators to foundation/aws/credentials and retire admin

GenerateAWSAccessKey and GenerateAWSSecretKey move unchanged to
foundation/aws/credentials as a stateless format primitive. handlers/iam
and operator/cli import it directly. The admin legacy root is deleted
and joins the retired roots so it cannot return.

* docs: cite the admin retirement in the migration record

* revert: remove the uncalled ADR-0007 codec and design stack from the ADR-0001 checkpoint

The instance transition-record codec, its tests and the slice 1a design
had no production caller. They leave this package-boundary checkpoint
and will be reapplied on a dedicated ADR-0007 branch. Slice 0
characterisation and the vpcd IMDS readers in
domains/ec2/instance/state stay.

* docs: cite the ADR-0007 codec revert in the migration record

* fix: declare the ECS action subjects in service-interfaces.yaml

Declaring ecs.bus.* made ecs a managed prefix for manifest-lint, which then
failed on the 34 ECS action subjects the daemon registers. Declare them as
ecs.*, the same wildcard convention as rds.*.

* chore: run manifest-check and manifest-lint in preflight

CI runs both, but preflight did not, so manifest drift could pass local
checks and still fail Build and Test.

* docs: record resumed preflight enforcement and dev merge ports

* fix: restore dev changes dropped by earlier merges

Reapplies the slices.ContainsFunc rewrite in nodeFitsCustomerInstance and the DeleteDBSubnetGroup doc comment, both lost when their files were refactored.

* docs: record accountteardown as a frozen legacy root pending cleanup capabilities

---------

Co-authored-by: Julian Sommer <115516918+juliansommer@users.noreply.github.com>
2026-10-15 14:42:53 +10:00

171 KiB
Raw Blame History

Package-boundary migration record

Status: Active implementation record. Decision: Mulga ADR-0001 — Spinifex Package Boundary and Layout. Scope: Spinifex source layout only. This record does not establish a new architecture decision, expand a customer-facing interface, or claim that the current implementation conforms to the target layout.

Purpose

This document records the implementation sequence for ADR-0001 in this repository. The ADR owns the target layout and dependency rules; this record captures the source-level entry-point inventory and the evidence needed for each narrow structural slice.

The migration proceeds on one long-lived refactor branch. Each commit remains small, independently buildable and reviewable; that does not require one pull request per package move.

Baseline

The inventory below was taken from Spinifex commit ecef5e4d2f793b4cf2c96d0a43d0a1ab37be3eed, the revision pinned by Mulga when ADR-0001 was accepted. It records ownership standing, not a promise that a binary or script is AWS-compatible or supported beyond its documented scope.

Entry-point and automation inventory

Product operator and delivery entry points

Current path Standing Target ownership Migration rule
cmd/spinifex (spx) Supported operator process Thin main; command assembly in spinifex/operator/cli and individual supported use cases in spinifex/operator/* Preserve binary and supported command compatibility. The CLI must reach a domain through an explicit capability, never its private state.
cmd/installer Delivery executable Thin main; interactive, unattended and host-installation work in spinifex/operator/installer Preserve installation behaviour while separating executable startup from delivery logic. It is not, merely by existing, the permanent administration interface.

cmd/spinifex/cmd and cmd/installer/* have since moved: commits 05f390e61 and 918150758 below separate each executable's startup from its command and installation logic, landing them in spinifex/operator/cli and spinifex/operator/installer/<same>.

Guest and named-role entry points

Current path Standing Target ownership Evidence of role boundary
cmd/ecs-agent ECS guest agent Thin main; spinifex/agents/ecs/agent Baked into ECS node images; communicates with the control plane through the AWS gateway, not NATS.
cmd/rds-agent RDS guest agent Thin main; spinifex/agents/rds/agent Runs in an RDS guest, handles bootstrap and engine directives through the gateway.
cmd/lb-agent ELBv2 guest agent Thin main; spinifex/agents/elbv2 Included in the load-balancer microVM and controls its local data plane.
cmd/eks-token-webhook EKS control-plane guest role Thin main; spinifex/agents/eks/tokenwebhook Baked into the EKS node image as the Kubernetes TokenReview webhook.
cmd/eks-gateway-fetch EKS control-plane guest helper Thin main; spinifex/agents/eks/gatewayfetch Fetches recovery and addon state through the AWS gateway.
cmd/eks-gateway-publish EKS control-plane guest helper Thin main; spinifex/agents/eks/gatewaypublish Relays bootstrap and state reports through the AWS gateway.
cmd/eks-webhook-cert EKS image-resident certificate helper Thin main; spinifex/agents/eks/webhookcert Invoked by EKS image scripts to create/reuse webhook serving certificates.
cmd/eks-konnectivity-cert EKS image-resident certificate helper Thin main; spinifex/agents/eks/konnectivitycert Invoked by EKS image scripts to create/reuse Konnectivity serving certificates.
cmd/ecr-credential-provider EKS node integration Thin main; spinifex/agents/eks/credentialprovider Kubelet exec credential provider baked into EKS images; it is not ECR control-plane ownership.

No common agent package is introduced by this inventory. Shared mechanisms are considered only when the existing role and least-privilege boundaries remain intact; ADR-0001 explicitly defers that design.

Development and qualification entry points

Current path Standing Target ownership Rule
cmd/aws-model-coverage Qualification generator Thin main; reusable code in internal/tooling Produces the dispatch-versus-model inventory; never imported by product packages.
cmd/dhcptest Development diagnostic Thin main; reusable code in internal/tooling if extraction is useful It is a DHCP probe, not a supported spx admin command.
cmd/spx-loadgen Qualification/load tool Thin main; reusable code in internal/testkit It drives test load and is not shipped to nodes.
tests/e2e/*/cmd/*, tests/bench/*/cmd/* Test and benchmark-private tools Remain test-private They are invoked by a test or benchmark harness, not a product process.
docs/terraform-workbooks/demo-app/main.go Demonstration workload Remain with the workbook It is neither a Spinifex executable nor a product package.

Automation standing

Path class Standing Rule during migration
.github/scripts/ CI-private qualification automation Keep at the module root; the invoking workflow owns the evidence scope.
build/, images/, scripts/images/, image build and publish scripts Delivery and guest-image inputs Keep at the module root. In-guest helpers stay with their image; do not relocate shell sources into Go packages.
scripts/setup.sh, setup-ovn.sh, install-node.sh, node-reset.sh, uninstall-spx.sh Transitional delivery and documented host-operation mechanics Keep at the module root. They may implement a published procedure but are not an enduring public administration API solely because they are installed on a node.
scripts/dev-env/, scripts/demo/, benchmarks, diagnostics and Terraform experiments Development or exploratory automation Keep at the module root; they are not customer-facing contracts.
scripts/check-coverage.sh, diff-coverage.sh, run-gate.sh, sync-aws-models.sh and test harness scripts Repository and qualification automation Keep at the module root; their caller defines the contract and evidence.

First source move — ARN

The first source move is the cohesive ARN leaf package:

  • old import path: github.com/mulgadc/spinifex/spinifex/arn
  • target import path: github.com/mulgadc/spinifex/spinifex/foundation/aws/arn

Source commit 91ac70e29 moves the ten package files and updates all forty-four Go importers. It changes no behaviour and leaves no forwarding package.

Verification passed with an isolated Go module/build cache:

  • go test ./spinifex/foundation/aws/arn
  • go test for the fourteen packages directly importing ARN
  • a Go-source search for the old import path returned no matches
  • git diff --check

The inventory was intentionally committed before this move so a later thin-main refactor cannot silently choose an owner by convenience.

Second source move — AWS filters

The second source move removes the generic filterutil location and name:

  • old import path: github.com/mulgadc/spinifex/spinifex/filterutil
  • target import path: github.com/mulgadc/spinifex/spinifex/foundation/aws/filters

Source commit e707be942 moves the two package files, renames the Go package to filters, and updates twenty-eight importing files. Callers use the explicit import alias awsfilters: many filter implementations already hold a local filters value, and the alias prevents a package-name collision without changing that local state or any filter behaviour.

Verification passed with an isolated Go module/build cache:

  • go test ./spinifex/foundation/aws/filters
  • go test for the seventeen direct caller packages
  • a Go-source search for the old import path returned no matches
  • git diff --check

Third source move — AWS Query parser

The third source move names the protocol concern rather than the first service that happened to use it:

  • old import path: github.com/mulgadc/spinifex/spinifex/awsec2query
  • target import path: github.com/mulgadc/spinifex/spinifex/ingress/aws/query

Source commit caaf98d9e moves the two parser files, renames the Go package to query, and updates thirteen importing files. The parser is used across EC2, IAM, STS, ECS, ECR, ELBv2 and RDS, but owns only AWS Query wire decoding and no service action or resource semantics.

Verification passed with an isolated Go module/build cache:

  • go test ./spinifex/ingress/aws/query
  • go test -run '^$' for six direct caller packages
  • a Go-source search for the old import path returned no matches
  • git diff --check

A broad go test run over those caller packages was stopped after two minutes without output. It is not recorded as a passing test; the normal full suite remains CI evidence for the branch.

Subsequent source moves

The following slices follow the same rule as the first three: each is a structural move with import rewrites, no compatibility shim, focused package and direct-caller validation, and git diff --check. They are recorded here while the branch is in flight; they do not describe the umbrella repository's main until its Spinifex gitlink is updated.

Source commit Old path New path Finding or boundary recorded
04b41a62c spinifex/idempotency spinifex/foundation/lifecycle/idempotency Generic request coalescing is lifecycle infrastructure, not a domain owner.
25fa01a3d spinifex/preflight spinifex/bootstrap/preflight Startup validation is bootstrap input, not a generic service.
681fccc61 spinifex/systemd spinifex/operator/host/systemd Host unit generation and reconciliation are operator-host work.
23ca5d8aa spinifex/resource spinifex/foundation/lifecycle/resource Resource metadata is a generic lifecycle primitive.
7a3c8de07 spinifex/nbd spinifex/runtime/compute/nbd NBD process control is node-local compute runtime.
ea65616be spinifex/hostdns spinifex/runtime/host/dns Host resolver integration is runtime host work.
d80803807 spinifex/qmp spinifex/runtime/compute/qmp QMP is local VM runtime control.
41e2c1f10 spinifex/gpu spinifex/runtime/compute/gpu GPU discovery and attachment are local compute runtime work.
d78246d05 spinifex/instancetypes spinifex/domains/ec2/instancetypes AWS-visible instance-type semantics belong to EC2.
b2ce46124 spinifex/service spinifex/runtime/service The named-role catalogue is runtime composition.
573ee6761 spinifex/loadgen internal/testkit/loadgen Load generation is qualification support, not product code.
afdb6618c spinifex/otelsetup spinifex/foundation/telemetry Spinifex instrumentation primitives are shared telemetry foundation.
956fe1490 spinifex/awserrors spinifex/foundation/aws/errors AWS error vocabulary is shared AWS foundation.
cbbf252e6 spinifex/testutil internal/testkit Test doubles remain compiler-private and cannot become application dependencies.
0c2d64413 spinifex/vm spinifex/runtime/compute/vm VM lifecycle is node-local compute runtime, not EC2 API ownership.
a74adc48b spinifex/ebsmetadata spinifex/domains/ec2/ebs/metadata EBS API metadata remains EC2-domain state.
94ddc91a1 spinifex/ebsprovider spinifex/providers/ebs The provider contract and conformance suite are replaceable storage adapters.
3c1bce1eb spinifex/cloud/oci spinifex/providers/cloud/oci OCI access is a replaceable cloud-provider adapter.
56970d079 spinifex/services/nats spinifex/runtime/roles/nats NATS process startup is a named runtime role.
d87326f64 spinifex/network/listenerinventory internal/testkit/listenerinventory Listener inventory is test-only qualification support.
4d4875b13 spinifex/services/spinifexui spinifex/runtime/roles/spinifexui The web UI is a named runtime role, not a logical AWS domain.
f39bc64c9 spinifex/admin listener helpers spinifex/foundation/netaddr Listener-address parsing is a small shared network primitive.
9e8e889fb spinifex/services/qemunbdd spinifex/runtime/roles/qemunbdd QEMU-NBD provider process startup is a runtime role.
3494ae131 spinifex/services/qmpcollector spinifex/runtime/roles/qmpcollector QMP collection is a named runtime role.
f64931546 spinifex/services/spinifex spinifex/runtime/roles/spinifex The core daemon executable role is runtime composition.
57f238a0c spinifex/tags spinifex/foundation/aws/tags System-tag vocabulary is shared AWS foundation.
7a6b00728, 186ef7ebc spinifex/kvutil spinifex/foundation/state/kvutil Replica policy was made explicit before moving generic KV utilities to state foundation.
57f9d2ccc spinifex/kvstore spinifex/foundation/state/kvstore Generic KV storage mechanics are state foundation.
247e52ba3 spinifex/reconciler spinifex/foundation/lifecycle/reconciler Generic reconciliation mechanics are lifecycle foundation.
b1d0eba89 inline KV migration mechanics spinifex/foundation/state/migrate KV migration is a reusable state concern.
967f6904a spinifex/kvlease spinifex/foundation/state/kvlease Leases are state foundation, not a resource-domain owner.
248f0e005, 8c4af3030 spinifex/formation spinifex/runtime/formation Formation owns its protocol payloads and remains runtime membership/bootstrap work.
b8b97279c, c0c64cd53 spinifex/instancecache spinifex/runtime/compute/cache Cache policy is decoupled from callers before becoming node-local compute runtime.
aa0c36ff4 spinifex/types/ec2.go contracts/ec2/v1 The EC2 instance-command subject and JSON payload are an explicit versioned cross-process contract; commands_test.go is its compatibility evidence.
60c9ed47d spinifex/types/types.go (IGWEvent) contracts/ec2/v1/igw.go (InternetGatewayEvent, InternetGatewayAttachSubject, InternetGatewayDetachSubject) The EC2-to-network internet-gateway attach/detach projection on vpc.igw-attach/vpc.igw-detach is an explicit versioned cross-process contract; igw_test.go pins its subject and JSON shape.
c239fd897 spinifex/types/eni.go spinifex/runtime/compute/vm/eni_requests.go PCIe hot-plug slot allocation is mutex-bearing, per-VM runtime state rather than a shared type or wire contract; record_test.go preserves its restart semantics.
b49e55a58 spinifex/types/node.go (NodeDiscoverResponse) contracts/cluster/v1/discovery.go (NodeDiscoverResponse, NodesDiscoverSubject) Fan-out node-membership discovery on spinifex.nodes.discover is a versioned cluster control-plane contract; discovery_test.go pins its subject and JSON shape.
6d6275e78 spinifex/types/cluster.go (NodeHealthResponse) contracts/cluster/v1/health.go Node health is a versioned cluster control-plane representation shared by the per-node NATS route and daemon HTTPS /health; health_test.go pins both wire forms.
53a614717 spinifex/types/cluster.go (SharedClusterData) spinifex/daemon/config_hash.go Shared-config hash input is daemon-local implementation data; focused config-hash tests preserve deterministic, shared-only semantics.
5d88a3399 spinifex/types/storage.go contracts/operator/v1/storage.go (StorageConfigResponse, StorageEncoding, StorageMetaNode, StorageBlobNode, StorageBucket) The spinifex.storage.config operator-plane response describing Predastore topology is a deployment-administration contract, not an AWS tenant-facing API; storage_test.go pins its subject and JSON shape.
78fd017e0 spinifex/types/telemetry.go (GuestTelemetryMeta) contracts/telemetry/v1/guest_meta.go QMP collector sidecar JSON is a versioned file contract between VM runtime and telemetry role; guest_meta_test.go pins its filename and payload.
4b35a2a33 spinifex/types/node.go (NodeVMsResponse, VMInfo, VMGPUInfo) contracts/cluster/v1/node_vms.go VM inventory is a versioned cluster fan-out payload used by SPX CLI and EKS host lookup; node_vms_test.go pins its subject and JSON shape.
08bd335f0 spinifex/types/node.go (NodeStatusResponse, capacity and GPU inventory) contracts/cluster/v1/status.go Node status is a versioned fan-out observation contract for operator inspection and EC2/EKS scheduling consumers; status_test.go pins its subject and JSON shape.
f4cb550c9 spinifex/types/ebs.go (GP3 policy constants) spinifex/domains/ec2/ebs/policy/gp3.go The supported GP3 product envelope is EC2 EBS domain policy, not generic runtime or Viperblockd wire state; gp3_test.go pins the deployed values.
adb55de33 spinifex/types/ebs.go (NBDTransport) spinifex/services/viperblockd/nbd_transport.go NBD endpoint transport selection is Viperblockd-local delivery configuration, not generic EBS state or a provider contract; nbd_transport_test.go pins the socket and TCP forms.
94a6e7af2 spinifex/types/ebs.go (EBSConfigUpdateRequest, EBSConfigUpdateResponse) contracts/viperblockd/legacy/v1/config_update.go Legacy configuration updates have deployed queue and volume-addressed NATS routes; the versioned compatibility contract is expressly transitional and separate from providers/ebs.
0b82e1404 spinifex/types/ebs.go (EBSSyncRequest, EBSSyncResponse) contracts/viperblockd/legacy/v1/sync.go The ebs.sync queue-group route that reloads a mounted volume's state from its backend is deployed Viperblockd legacy compatibility, not generic EBS state; sync_test.go pins its subject and JSON shape.
b975f1ba9 Remaining spinifex/types/ebs.go payloads runtime/compute/vm/ebs_requests.go and contracts/viperblockd/legacy/v1/volume.go The mutex-bearing attachment collection is VM-owned; deployed mount, unmount and delete routes plus their JSON payloads are versioned Viperblockd legacy compatibility. Contract tests pin every route and JSON shape.
08d950770 spinifex/types/telemetry.go (MetricsEC2SubjectPrefix, TelemetrySeries) spinifex/runtime/roles/qmpcollector/metrics.go The guest-metrics subject prefix has no versioned contract yet: its eventual consumer is Goanna, so it stays collector-local runtime data pending a cross-repository contract decision rather than being promoted to contracts/. This empties and removes spinifex/types; the package no longer exists in this repository.
61cf833aa spinifex/cloud/exoscale spinifex/providers/cloud/exoscale Exoscale CLI access is a replaceable external-cloud adapter. The source move rewrites its four Exonet callers and preserves its adapter tests and CLI boundary.
dbc62562c spinifex/objectstore spinifex/providers/objectstore The generic S3-compatible client is a replaceable object-storage adapter, not S3 resource authority. The mechanical move rewrites all direct callers; its focused suite and caller compilation passed.
f11922986 spinifex/config spinifex/bootstrap/config Root configuration parsing belongs to bootstrap. Its package API is unchanged; package tests and regular, integration, E2E and benchmark caller compilation passed.
aa4b0b1bb spinifex/paging spinifex/foundation/aws/paging Opaque listing tokens, deterministic slicing and EC2 pagination validation are shared AWS protocol infrastructure rather than EC2 resource authority. The move rewrites all five EC2 callers; focused package tests, direct caller compilation and tagged integration compilation passed.
6ac02a886 spinifex/clustersize spinifex/foundation/state/clustersize The declared cluster size and its fail-closed JetStream/KV replica policy are shared state-tier infrastructure, not a resource-domain owner. Focused package tests, all direct caller compilation, integration and E2E-tagged harness compilation passed.
93084043a spinifex/handlers/ecr spinifex/domains/ecr ECR repository metadata, blob semantics and lifecycle rules are ECR domain ownership. The full domain suite, direct callers and tagged integration callers passed.
51b5c55b9 spinifex/gateway/ecrapi spinifex/domains/ecr/awsapi AWS JSON 1.1 action inventory, validation, resource-scope derivation and NATS-backed action handlers are ECR action-adapter ownership. Focused, gateway and tagged integration validation passed.
5e7311eb2 spinifex/gateway/ecr spinifex/domains/ecr/registry The OCI Distribution v2 protocol adapter is ECR data-plane ownership. Focused registry, gateway/AWS-gateway and tagged integration validation passed.
474c07316 spinifex/gateway/ecrauth spinifex/domains/ecr/auth ECR token claims, issuer/verifier, encrypted signing-key persistence and rotation are ECR credential ownership. Focused, gateway/AWS-gateway and tagged integration validation passed.
5ff0fa452 Gateway buildRepository, ecrRepositoryArn, ecrRegistryHost, ecrRepositoryUri (GatewayConfig methods) spinifex/domains/ecr/awsapi.RepositoryEndpoint (RepositoryARN, RegistryURIHost, RepositoryURI, RepositoryFromMeta) ECR repository ARN, registry URI and AWS response projection are ECR-domain composition state; the endpoint profile is supplied by the composition root rather than read from GatewayConfig fields.
f59b40ad1 Gateway DescribeRepositories action semantics spinifex/domains/ecr/awsapi.DescribeRepositories Request validation, account scope, repository metadata lookup and AWS response projection are ECR AWS-action ownership. Gateway retains authenticated HTTP adaptation and response writing. Focused domain, gateway dispatch and tagged integration compilation passed.
c558872e8 Gateway CreateRepository action semantics spinifex/domains/ecr/awsapi.CreateRepository Request decoding, input defaults and validation, duplicate detection, account-scoped metadata persistence and AWS response projection are ECR AWS-action ownership. Gateway retains authenticated HTTP adaptation and response writing. Focused domain, gateway lifecycle and tagged integration validation passed.
a6b209b7b Gateway DeleteRepository action semantics spinifex/domains/ecr/awsapi.DeleteRepository Request validation, account scope, non-empty protection, forced deletion, metadata cascade and AWS response projection are ECR AWS-action ownership. Blob reclamation remains the separately documented GC concern. Focused domain, gateway lifecycle and tagged integration validation passed.
7a5159366 Gateway PutImageTagMutability action spinifex/domains/ecr/awsapi.PutImageTagMutability This endpoint-free action is registered directly in the ECR action table rather than retained as a gateway wrapper. Request validation, account-scoped metadata update and AWS response construction are domain-adapter ownership; generic gateway auth and policy gating remain before dispatch. Focused domain, gateway and tagged integration validation passed.
a2f6005d2 Gateway GetAuthorizationToken action response spinifex/domains/ecr/awsapi.GetAuthorizationToken Gateway retains canonical IAM/STS principal construction from authenticated context. The ECR adapter consumes a narrow token-issuer capability and owns mint-result handling, Docker AWS:<jwt> encoding and AWS response projection. Focused domain, real issuer/verifier gateway round-trip and tagged integration validation passed.
ee4610a75 Gateway ListImages action semantics spinifex/domains/ecr/awsapi.ListImages The ECR AWS adapter consumes a narrow OCI image-catalog capability and owns request validation, account scope, tag-status projection and AWS response/error mapping. Gateway retains authenticated HTTP adaptation and supplies the configured registry. Focused domain, registry-backed gateway and tagged integration validation passed.
b22b723a4 Gateway DescribeImages action semantics spinifex/domains/ecr/awsapi.DescribeImages The ECR AWS adapter now shares the image-catalog capability and catalog error mapping with ListImages, and owns image selection/detail projection. The accepted-but-unapplied filter field is explicitly retained as an unchanged compatibility gap. Focused domain, registry-backed gateway and tagged integration validation passed.
deabaa300 Gateway BatchGetImage action semantics spinifex/domains/ecr/awsapi.BatchGetImage The ECR AWS adapter consumes a narrow manifest-reader capability and owns request validation, the 100-image cap, digest-over-tag selection and partial-failure response projection. Repository/account scope validation is now shared with ListImages and DescribeImages. Focused domain, registry-backed gateway and tagged integration validation passed.
2542ce98e Gateway PutImage action semantics spinifex/domains/ecr/awsapi.PutImage The ECR AWS adapter consumes a narrow manifest-writer capability and owns request validation, tag/digest selection, AWS response projection and OCI manifest-store error translation. Gateway retains authenticated HTTP adaptation and supplies the configured registry. Focused domain, registry-backed gateway and tagged integration validation passed.
7666ed208 Gateway BatchDeleteImage action semantics and residual ECR helpers spinifex/domains/ecr/awsapi.BatchDeleteImage, awsapi.ValidateRepositoryScope, gateway ecr_request.go The ECR adapter consumes a narrow image-deleter capability and owns batch deletion/partial-failure projection. Completion exposed scope validation still needed by lifecycle-preview adapters, which now calls the exported ECR adapter utility; generic HTTP JSON decoding moved to a gateway ECR adapter helper. Focused domain, image/lifecycle gateway and tagged integration validation passed.
72c20bf78 Gateway lifecycle-preview evaluation and StartLifecyclePolicyPreview spinifex/domains/ecr/awsapi.EvaluateLifecyclePreview, StartLifecyclePolicyPreview Shared preview evaluation now consumes explicit policy-store and image-catalog capabilities, owns request/stored-policy resolution and lifecycle evaluation, and returns a reusable expiry set. Start preview is fully domain-owned; Get preview consumes the evaluator and retains only its response projection pending the next slice. Focused domain, lifecycle gateway and tagged integration validation passed.
68945bbb1 Gateway GetLifecyclePolicyPreview response projection spinifex/domains/ecr/awsapi.GetLifecyclePolicyPreview Get preview now joins Start preview in the ECR adapter, sharing the synchronous evaluator and explicit policy-store/image-catalog capabilities. Gateway retains only authenticated HTTP/body adaptation and capability supply. Focused domain, lifecycle gateway and tagged integration validation passed.
4c9f3d681 Gateway ECR image-action wrappers and residual JSON adapter spinifex/domains/ecr/awsapi.RegistryActionService ListImages, DescribeImages, BatchGetImage, PutImage and BatchDeleteImage now dispatch through one composed registry-action capability after the generic gateway's authentication and policy gate. The composition root supplies narrow OCI capabilities; the operation inventory derives their implemented status from that capability. The obsolete gateway wrappers and JSON helper are removed. Focused domain, gateway, AWS-gateway and tagged integration validation passed.
40e7b14ad Gateway lifecycle-preview wrappers spinifex/domains/ecr/awsapi.LifecyclePreviewActionService StartLifecyclePolicyPreview and GetLifecyclePolicyPreview now dispatch through one composed policy-store-plus-image-catalog capability after generic authorization. Production reuses the registry's NATS metadata client; focused tests compose an in-memory metadata store. The obsolete wrappers are removed, and a missing composition fails closed rather than falling through to the raw action table. Focused domain, gateway, AWS-gateway and tagged integration validation passed.
54f96ad78 Gateway CreateRepository, DescribeRepositories and DeleteRepository wrappers spinifex/domains/ecr/awsapi.RepositoryActionService Repository actions now dispatch through one composed metadata-store-plus-endpoint-profile capability after generic authorization. The individual action adapters depend on a narrow repository metadata contract rather than constructing NATS clients themselves. The three obsolete wrappers are removed, and a missing composition fails closed. Focused domain, gateway, AWS-gateway and tagged integration validation passed.
5ebbdcb53 Gateway GetAuthorizationToken wrapper and endpoint adapter spinifex/domains/ecr/awsapi.AuthorizationTokenActionService GetAuthorizationToken now dispatches through a composed issuer-plus-endpoint capability after gateway constructs the canonical IAM/STS principal. The token subject remains gateway identity work; ECR retains credential response projection. The obsolete wrapper and endpoint adapter are removed, a missing action service fails closed, and an unavailable issuer retains the declared NotImplemented response. Focused domain, gateway, AWS-gateway and tagged integration validation passed.
2b945ad88 spinifex/handlers/acm, spinifex/gateway/acm spinifex/domains/acm, spinifex/domains/acm/awsapi ACM certificate state, issuance and renewal are ACM-domain ownership; its AWS JSON/NATS request adapters live beside them. All nine adapters, including the previously inlined RequestCertificate path, now use domains/acm/awsapi; generic gateway retains only routing, request context, policy gating and response writing. Focused domain, adapter and gateway request tests plus direct-caller and tagged integration compilation passed.
47c6b2fcf spinifex/gateway/bodyscope spinifex/ingress/aws/bodyscope Case-aware, ambiguity-safe extraction of AWS JSON request identifiers is ingress policy machinery, not generic gateway ownership. Focused parser tests and compilation of all six direct service-policy consumers passed.
d3cd6d520 spinifex/gateway/policy spinifex/foundation/aws/policy AWS wire-service to IAM action-name mapping is protocol-neutral AWS vocabulary. Its focused suite and direct gateway/RDS/integration caller compilation passed.
be7f0b1d0 spinifex/utils/platform spinifex/domains/ec2/platform EC2 Platform and UsageOperation derivation is EC2 compatibility vocabulary, not a generic helper. Focused package and metadata-default tests, plus direct daemon/image/instance caller compilation, passed.
e2ac3dbed spinifex/utils/command spinifex/runtime/host/command Bounded host-command execution is node-local runtime machinery. Focused command and host-DNS tests plus the direct SPX CLI caller compilation passed.
3360382f5 spinifex/utils/idempotency spinifex/foundation/lifecycle/idempotency Retry-token HTTP/NATS vocabulary now lives beside the idempotency store. Focused propagation and EIP retry behavior tests, affected package compilation and tagged integration compilation passed.
b5bc8cd7d spinifex/utils/images (SelectNewestImage) spinifex/foundation/aws/ami Selecting the newest EC2 image from AWS SDK models is shared AWS-model logic used by EKS, ECS, RDS and Bedrock, not operator catalogue ownership. Focused selector and remaining catalogue tests plus all four direct caller compilations passed.
9f83f899a spinifex/utils (ClientIP, RequestClientIP) spinifex/ingress/http HTTP remote-address parsing and loopback-trusted X-Real-IP handling are generic ingress policy shared by the AWS gateway and UI proxy. The moved test suite and all direct caller compilations passed.
693109950 spinifex/utils (HashMAC) spinifex/network/identifiers Deterministic locally-administered MAC derivation is network infrastructure shared by ENIs, DHCP, OVN topology, IMDS and VM tap setup. Focused MAC tests and every direct caller compilation passed.
79cf93996 spinifex/utils (DistinctIDs) spinifex/foundation/aws/identifiers Request-order-preserving normalization of AWS resource-ID lists is shared API-model handling used by image and snapshot enumeration. Focused tests and both direct caller compilations passed.
7e29f2640 spinifex/utils (download and terminal-progress helpers) spinifex/operator/progress Operator-facing downloads, terminal progress rendering and byte display are CLI presentation concerns. Focused tests and the spx admin compilation passed.
56471479f spinifex/utils (ValidateKeyPairName) spinifex/handlers/ec2/key EC2 key-pair-name validation has one production owner and is now private to it. Its moved unit matrix and full key-handler suite passed.
cf531cd73 spinifex/utils (QMP telemetry filename convention) contracts/telemetry/v1 The telemetry QMP prefix and metadata path are part of the existing VM-runtime-to-collector file contract. The contract test, focused VM telemetry tests, collector suite and caller compilations passed; the broader VM suite is sandbox-blocked by unrelated socket setup.
83615f1f4 spinifex/utils (AWS XML response helpers) spinifex/foundation/aws/xml EC2 XML response normalization/enveloping and IAM-style Query envelopes are shared AWS protocol mechanics. Focused XML tests and all direct caller compilations passed; the RDS-agent package was compile-checked without execution because its FIPS bootstrap requires a FIPS-enabled runtime.
9896834b2 spinifex/utils (ExtractTags, MapToEC2Tags) spinifex/foundation/aws/tags Generic EC2 tag-specification extraction and API-model projection join the AWS tag foundation; KV tag mirroring remains separate state-layer work. Focused tag tests and direct caller compilations passed; the volume package was compile-checked without execution because its test main opens a sandbox-blocked TCP listener.
d8985c287 spinifex/utils (AccountKey) spinifex/foundation/state/kvutil Account-scoped durable-key construction belongs to the state foundation; its {accountID}.{resourceID} layout is unchanged. AWS account policy (GlobalAccountID, IsAccountID) remains separate legacy utility work. Focused key and tag-mirror tests plus every direct resource-caller compilation passed.
e6794d87d spinifex/utils (SudoCommand, NeedsPrivilege, EndpointSysctlHelper) spinifex/runtime/host/command Host-command privilege escalation is node-local runtime machinery and now sits beside bounded command execution. The RG-10 invariant restricting direct sudo construction to this package and network/host/run.go moves with it. Focused privilege, preflight, network-host, host-DNS and vpcd tests, every direct caller compilation and tagged integration/E2E compilation passed.
fd6c12f14 spinifex/utils (PID files, runtime directory, socket waits, process signalling and OOM score) spinifex/runtime/host/process Node-local process supervision shared by the daemon, VM runtime, Viperblockd and every named role is runtime host machinery. The remaining NBD endpoint helpers temporarily import it until they move to compute runtime. The shipped Viperblock unit comment still names utils.KillProcess; it is left unchanged so a structural move does not alter a delivered unit. Full preflight (excluding the pre-existing containerd advisory), focused caller suites and tagged integration/E2E compilation passed.
e712c6a7f spinifex/utils (NBD socket paths, URI format/parse, readiness wait) spinifex/runtime/compute/nbd NBD endpoint naming and readiness join the node-local NBD runtime, removing the interim utils to runtime-host edge. The EBS provider conformance suite now imports compute runtime for the URI format; if a versioned EBS wire contract is introduced, that format is a candidate for contracts/ebs/v1. Full preflight (excluding the pre-existing containerd advisory), focused NBD, VM, Viperblockd, QEMU-NBD, EBS-provider and admin suites, and tagged integration/E2E compilation passed.
366e2c2e4 spinifex/utils (image catalogue, checksum verification, disk-image extraction) spinifex/operator/imagecatalog The published image catalogue and its download verification serve only spx admin image import, an operator use case. The package is named imagecatalog because the command tree already uses images as a local identifier. Moved tests reach repository fixtures one directory deeper; script and documentation pointers to the catalogue file were updated. Full preflight (excluding the pre-existing containerd advisory), focused catalogue and CLI suites, and tagged integration/E2E (including GPU) compilation passed.
53f13f213 spinifex/utils (GenerateResourceID) spinifex/foundation/aws/identifiers AWS-style {prefix}-{17 hex} resource-ID generation joins the AWS identifier foundation beside request-ID normalisation; callers use the established awsidentifiers alias, which also avoids the network-identifier package name. Format and CSPRNG failure handling are unchanged. Full preflight (excluding the pre-existing containerd advisory), focused tests and tagged integration/E2E compilation passed.
eab631b42 spinifex/services/predastore spinifex/runtime/roles/predastore Predastore process configuration and startup are a named runtime role; the package holds no S3 resource semantics. The service manifest path moved with it. Full preflight (excluding the pre-existing containerd advisory), focused role, role-catalogue and CLI suites, and tagged integration/E2E compilation passed. Commit 796fb52d4 separately corrects the manifest's stale handlers/acm path left by the ACM relocation; manifest-check now passes.
ad6a158f8 spinifex/services/northstar spinifex/runtime/roles/northstar Northstar process startup and reload are a named runtime role. BootstrapBaseZone moved with it unchanged but is DNS control-plane work (it imports handlers/dns); it belongs to domains/dns when that domain moves, so the role is not yet thin. Full preflight (excluding the pre-existing containerd advisory), focused role, role-catalogue and CLI suites, manifest-check and tagged integration/E2E compilation passed.
931d86d5c spinifex/services/awsgw spinifex/runtime/roles/awsgw The AWS gateway process is a named runtime role and its composition root wires every domain, which runtime may do. The quota reconcile loop (runQuotaReconcile, quotaKeyFor) moved with it unchanged but belongs to domains/admission/quota, and the EKS token-verify responder belongs with STS or EKS identity; both are recorded residuals, so the role is not yet thin. Service-interface manifest, design doc and architecture diagram paths were updated. Full preflight (excluding the pre-existing containerd advisory), focused role, role-catalogue and CLI suites, manifest-check and tagged integration/E2E compilation passed.
745338563 spinifex/utils (GenerateErrorPayload, GenerateErrorPayloadWithMessage, ValidateErrorPayload, UnmarshalJsonPayload) spinifex/foundation/aws/errors The JSON error envelope every domain exchanges over NATS carries the shared AWS error vocabulary, so it sits beside the codes it encodes rather than in a per-domain contract. Behaviour is unchanged. Full preflight (excluding the pre-existing containerd advisory), focused error and caller suites and tagged integration/E2E compilation passed.
02e0dd0dc spinifex/utils (GlobalAccountID, IsAccountID) spinifex/foundation/aws/identifiers The 12-digit account ID check and the system account ID constant are AWS identifier vocabulary and sit beside resource ID generation; no account policy changed. Full preflight (excluding the pre-existing containerd advisory), focused identifier and caller suites and tagged integration/E2E compilation passed.
b5806ce63 spinifex/utils (NATS connect/retry, request/serve, gather, headers, PublishEvent, trace propagation) spinifex/foundation/messaging/nats (package natsmsg) Generic NATS transport primitives are shared machinery with no resource owner; the package name avoids colliding with nats.go at every caller. The OTel instrumentation scope keeps its spinifex/utils value so existing APM queries still match. vpcd's request/ack protocol (RequestEvent, AddNAT, PublishNATEvent) stays in utils/vpcd_event.go for a future network contract, and SubjectEnsureDefaultVpc stays in utils/ec2_subject.go for an EC2 contract; both were split out byte-identical. Full preflight (excluding the pre-existing containerd advisory), focused transport and caller suites and tagged integration/E2E compilation passed.
cabe0e99b spinifex/handlers/sysinstance spinifex/domains/ec2/systeminstance System-instance launch is compute ownership; managed services (ELBv2, EKS, RDS, Bedrock) consume the launcher contract. The package was renamed to its directory; runtime sysinstance: error strings are unchanged. Managed-service parent metadata is still only the ManagedBy tag value, short of the explicit parent metadata the ADR names. Full preflight (excluding the pre-existing containerd advisory), focused daemon and managed-service suites and tagged integration/E2E compilation passed.
4d86296eb spinifex/handlers/systemvpc spinifex/domains/network/systemvpc Managed system-VPC realisation is network ownership; ELBv2, EKS, RDS and Bedrock consume its builder. The package was renamed from handlers_systemvpc to its directory and callers dropped the alias; it depends only on foundation packages. Full preflight (excluding the pre-existing containerd advisory), focused system-VPC, daemon and managed-service suites and tagged integration/E2E compilation passed.
09de514ef spinifex/handlers/imds spinifex/domains/ec2/guestmetadata Guest metadata is a compute-domain interface served to guests on each node. The package was renamed from handlers_imds to its directory and its vpcd and E2E callers dropped the alias; five path comments were updated. The move exposes dependency-rule breaks it does not repair: imports of the DNS, EC2 instance, EC2 key, IAM and STS implementations, and the first domain-to-runtime edge (runtime/compute/vm); see Guest metadata relocation residuals. Full preflight (excluding the pre-existing containerd advisory), focused guest-metadata, vpcd and DNS suites and tagged integration/E2E compilation passed.
99278d5ff spinifex/handlers/dns spinifex/domains/dns DNS control semantics (zone records, resolver and name policy) are DNS-domain ownership, distinct from OVN and host-network realisation. The package name was already dns, so callers dropped the handlers_dns alias; the multinode E2E test that also imports miekg/dns uses dnsdomain. Six path comments were updated. The move exposes imports of runtime/compute/vm, network/reconcile and bootstrap/config, recorded in the dependency-rule debt. The integration,segscanoracle build fails identically on origin/dev (predastoreS3Client is in package integration but called from integration_test) and was not touched. Full preflight (excluding the pre-existing containerd advisory), focused DNS, ACM, guest-metadata, daemon, northstar, admin and CLI suites, manifest-check and tagged integration, E2E and E2E bench compilation passed.
06b1eefd9 spinifex/runtime/roles/northstar (BootstrapBaseZone) spinifex/domains/dns Seeding the base zone and its NS topology is DNS control-plane work, so it joins the DNS domain and the northstar role is left with process startup and reload only. Its tests moved with it and reuse the DNS package's identical fakeS3 helper. The daemon flakes seen during verification (TestRestoreInstances_ClusterRecordWinsForSharedInstance under load; TestKVIPSecBarrier_* and TestDrainVolumeRecordsItsPointAfterTheWork under -count=3) reproduce identically on origin/dev. Full preflight (excluding the pre-existing containerd advisory), focused DNS, northstar and CLI suites and tagged integration/E2E compilation passed.
8ca318070 spinifex/admin (SystemAccountID) spinifex/foundation/aws/identifiers (GlobalAccountID) SystemAccountID() duplicated the existing GlobalAccountID constant (000000000000), so its callers now use the constant and the function and its test are deleted rather than moved. DefaultAccountID and DefaultAccountName are Mulga bootstrap identity policy, not AWS identifier syntax, and stay in admin for a later IAM/account bootstrap slice. Full preflight (excluding the pre-existing containerd advisory), focused admin, account-teardown, EKS, CLI and identifier suites and tagged integration/E2E compilation passed.
930040875 spinifex/admin (LoadCAKeyPair) spinifex/foundation/pki Loading a PEM certificate and PKCS8 RSA key is a generic primitive; the new package imports no Spinifex package and holds no certificate identity or file layout. GenerateCACert and GenerateSignedCert stay in admin because they embed Spinifex certificate identity and validity policy. Full preflight (excluding the pre-existing containerd advisory), focused admin and RDS suites and tagged integration/E2E compilation passed.
001aa2d42 spinifex/admin (TenantCACertPath, TenantCAKeyPath) spinifex/domains/acm The tenant private CA's on-disk layout belongs to ACM, beside LoadTenantCA and LoadOrCreateTenantCA, which consume it; the daemon and the spx admin acm command now call the ACM domain. Full preflight (excluding the pre-existing containerd advisory), focused ACM, admin, daemon and CLI suites and tagged integration/E2E compilation passed.
454defb00 spinifex/admin (IPsec certificate paths, peer issuance, strongSwan CA install and reread) spinifex/network/host IPsec peer credentials belong with the host IPsec owner that consumes them; network/host no longer imports admin. Peer keys use their own ipsecCertKeyBits seam (4096, lowered in tests) instead of admin's certKeyBits, a deliberate duplicate of the key-size policy. The moved test now builds its throwaway CA locally instead of calling admin's GenerateCACert. Full preflight (excluding the pre-existing containerd advisory), the four moved tests, focused network-host, admin and CLI suites and tagged integration/E2E compilation passed.
2c467988d spinifex/handlers/bedrock spinifex/domains/ochre Ochre is the platform service and owns model serving placement, launch, readiness, capacity and reaping; the Bedrock name now marks only the AWS API surface, per the amended ADR-0001 map row. Package renamed from handlers_bedrock to ochre, callers drop the alias, and KV bucket names, NATS subjects, metric names and error strings keep their bedrock names. The comment in the shipped bedrock-serve image script still names the old path because editing it changes image build input. Full preflight apart from the known govulncheck finding, focused Ochre, vector, Bedrock gateway, daemon, account teardown, awsgw, config and CLI tests, and tagged integration/E2E compilation passed.
93f101c0f spinifex/handlers/ochrevector spinifex/domains/ochre/vector The tenant vector store (pgvector backend, KV index registry, ingest, jobs, backup and appliance orchestration) is Ochre domain state and now sits under the Ochre domain as the ADR-0001 map requires. Package renamed from handlers_ochrevector to vector with its existing package comment; callers use the ochrevector alias because vector is a common local identifier in the gateway, daemon and CLI. Error strings keep their ochrevector: prefix. The service-interface manifest names the new path. Full preflight apart from the known vulnerability-scan finding, focused Ochre, gateway, daemon, awsgw and CLI tests, tagged integration/E2E compilation and the manifest check passed.
2b6131e21 spinifex/gateway/bedrock/hfhub spinifex/domains/ochre/hfhub The Hugging Face Hub client resolves and streams model weights for Ochre and has no dependency on the rest of gateway/bedrock; its only caller is spx admin ochre weights pull. Package name unchanged. The rest of gateway/bedrock is not moved: it holds Ochre domain state, stores and the inference engine alongside the Bedrock action layer, and domains/ochre imports it, so moving it to domains/ochre/awsapi would invert the domain-to-adapter direction. It is recorded as dependency-rule debt. Full preflight apart from the known vulnerability-scan finding, focused hfhub and CLI tests, and tagged integration/E2E compilation passed.
40964815b spinifex/utils (MergeTagsMut, ApplyTagRemovals, RemoveTagsMut, UpdateKVRecordTags, MirrorKVRecordTags) spinifex/domains/ec2/tagmirror EC2-wide AWS tag mutation over resource KV records is EC2 domain behaviour and imports only foundation packages. The EC2 tags resource package was not used because it imports the instance, key and VPC resources that call these helpers, which would create a resource-to-resource cycle. Twelve EC2 resource packages now call tagmirror; a stale utils.ParseNBDURI comment in the VM device code now names nbd.ParseNBDURI. Full preflight apart from the known vulnerability-scan finding, focused EC2, utils and VM tests, and tagged integration/E2E compilation passed.
7003168bc spinifex/network/* (all 15 packages) spinifex/domains/network/* Network control, policy, topology, OVN, external and host realisation are network-domain ownership. The packages move as one enclosing move because the L0–L5 invariants classify layers by trimming a single networkRoot prefix; a partial move would silently drop moved packages out of S1. Package names, layer assignments, reconciliation authority and the vpcd boundary are unchanged. Invariant scan roots, the RG-10 sudo allow-list path, .github/CODEOWNERS and the IMDS netns unit test's repo-root walk follow the new path. Injected host→topology and L3 CreateLogicalSwitch violations failed S1 and S5 at the new paths, confirming the invariants still scan the moved packages; domains/network/systemvpc now also falls inside the host-containment and NAT-mode scans and passes them. Full preflight apart from the known vulnerability-scan finding, focused network, invariant, host-command and vpcd suites, setup-ovn_test.sh, manifest-check and tagged integration/E2E compilation passed.
d8ab4017a spinifex/handlers/ec2/{account,key,launchtemplate,placementgroup,spotinstance,volumestate} spinifex/domains/ec2/<resource> These EC2 resources import no other EC2 handler package, so they move first as EC2 domain resources with their responsibilities unchanged. Each package is named after its directory and callers import it as ec2<resource>, because the bare names collide with common local identifiers; volumestate already carried its directory name. No package name appears in error text, KV keys or metrics. Path comments were updated. Full preflight apart from the known vulnerability-scan finding, focused EC2 domain, EC2 gateway, account-teardown and remaining EC2 handler suites, and tagged integration/E2E compilation passed.
20b5deb5c spinifex/handlers/ec2/{vpc,eigw,eip,igw,natgw,routetable} spinifex/domains/ec2/<resource> VPC, egress-only and internet gateways, Elastic IPs, NAT gateways and route tables are EC2 API resources and keep their responsibilities. vpc moves whole; splitting it along VPC, subnet, ENI and security-group boundaries remains EC2 decomposition work. Packages are named after their directories and imported as ec2<resource>; one test's local vpc alias was kept. The reconcile drift-watch test located bucket constants by matching the handlers_ec2_ qualifier text, so its pattern now matches ec2<resource>; removing ec2eip.KVBucketEIPs from the watched list made it fail as intended. domains/network's imports of these packages are now visible domain-to-domain edges, already listed in the dependency-rule debt. Full preflight apart from the known vulnerability-scan finding, focused EC2 and network domain, EC2 gateway, handler, daemon and CLI suites, and tagged integration/E2E compilation passed.
5d60184e8 spinifex/handlers/ec2/{instance,volume,snapshot,image,tags} spinifex/domains/ec2/<resource> Instances, volumes, snapshots, images and tags are EC2 API resources and keep their responsibilities; handlers/ec2 is now empty. Packages are named after their directories and imported as ec2<resource>; one test's local instance alias was kept. docs/service-interfaces.yaml mapped spinifex/handlers/ec2 to the daemon for e2e suite selection, so since the first EC2 slice an EC2-only change fell back to the default suites; a throwaway commit changing only a domains/ec2/key file selected e2e-cert e2e-lb with the old manifest and the daemon suites with the new one, which lists the 17 moved resource paths rather than all of domains/ec2 so selection is unchanged. The move exposes domains/ec2/instance's runtime and DNS imports, recorded in the dependency-rule debt. Full preflight apart from the known vulnerability-scan finding, focused domain, EC2 gateway, handler, daemon, account-teardown, CLI and selector suites, manifest-check and tagged integration/E2E compilation passed.
4501dbf69 cmd/ecr-credential-provider (implementation) spinifex/agents/eks/credentialprovider The kubelet exec credential provider is EKS-node integration and moves under the agent tree as ADR-0001 maps it; it depends only on internal/{ecrauth,guestenv,imdscreds}, so no ECR domain code gains a guest-environment dependency. main() became Main(in, out) int with the same body writing to out and returning the exit code; the thin cmd/ecr-credential-provider main keeps the binary name and the fipsboot import. .golangci.yml excludes gosec for cmd/, so the move brings this code under gosec for the first time; its one G101 false positive is annotated. The moved test, full preflight apart from the known vulnerability-scan finding, a bad-CA smoke run (empty-auth response, exit 1) and a dependency check that fipsboot still links into the binary passed.
fecf0b28a cmd/eks-gateway-publish (implementation) spinifex/agents/eks/gatewaypublish The control-plane VM's gateway publication relay is EKS in-guest behaviour. cmd/eks-gateway-publish keeps flag definitions, EKS_* environment defaults, parsing, the fipsboot import, the eks-gateway-publish: log prefix and exit code 1; the agent exposes only Config and Run(ctx, Config) error and imports no flag, os.Args, os.Stdin or os.Exit. The 11 former fatal sites had no defers or cleanup and return errors with identical text. retryDelay became a variable for tests and the retry wait honours context cancellation, which the binary's background context never triggers. New focused Run tests cover every validation and payload error, a wrapped publish, the 30-attempt budget and cancellation. Old and new binaries gave identical stderr and exit codes for nine command-path cases apart from the binary path in flag usage output. Full preflight apart from the known vulnerability-scan finding passed.
8929ee47b cmd/eks-gateway-fetch (implementation) spinifex/agents/eks/gatewayfetch The control-plane VM's add-on and recovery fetch is EKS in-guest behaviour. cmd/eks-gateway-fetch keeps its nine flags in their original order with EKS_* environment defaults, parsing, the fipsboot import, the eks-gateway-fetch: log prefix and exit code 1; the agent exposes only Config (with Out supplied by the binary) and Run(ctx, Config) error. The 8 former fatal sites had no defers or cleanup and return errors with identical text; a render failure is still not retried. As with publish, retryDelay is a test variable and the retry wait honours a cancellation the binary never triggers. The TSV emitters and their tests moved unchanged; new Run tests cover the five startup errors, addons and recovery rendering at their gateway paths, an unretried render failure, the attempt budget and cancellation. Old and new binaries gave byte-identical stdout, stderr and exit codes for 11 cases against a local TLS gateway, including both TSV outputs and flags overriding environment. Full preflight apart from the known vulnerability-scan finding passed.
d064924e6 cmd/eks-webhook-cert (implementation) spinifex/agents/eks/webhookcert Minting and caching the in-cluster admission webhook's self-signed serving certificate is EKS in-guest behaviour. cmd/eks-webhook-cert keeps the -dir, -cn and -dns flags, parsing, the usage text, the fipsboot import, the eks-webhook-cert: log prefix and exit code 1; the agent exposes only Config (with Out supplied by the binary) and Run(ctx, Config) error and depends only on the standard library. The 3 former fatal sites had no defers or cleanup; a key-write failure still leaves tls.crt on disk, and a stdout write error is still ignored. Moving out of cmd/ brought the file under gosec, whose G306 finding on the public certificate's 0644 mode is annotated with its reason. Existing tests moved unchanged; new Run tests cover the required-flag and blank-SAN errors, the cache-directory failure and the one-line CA/leaf/key output. Old and new binaries gave identical stdout, stderr, exit codes, file modes and output shape for 7 cases, and a second run reused the cached pair. Full preflight apart from the known vulnerability-scan finding passed.
fba330b86 cmd/eks-konnectivity-cert (implementation) spinifex/agents/eks/konnectivitycert Issuing and caching the on-node konnectivity-server certificate from the K3s cluster CA is EKS in-guest behaviour. cmd/eks-konnectivity-cert keeps its five flags and defaults (including -cn konnectivity-server), parsing, the usage text, the fipsboot import, the eks-konnectivity-cert: log prefix and exit code 1; the agent exposes only Config (with Out supplied by the binary) and Run(ctx, Config) error and depends only on the standard library. The 3 former fatal sites had no defers or cleanup; a key-write failure still leaves tls.crt on disk, and a stdout write error is still ignored. The gosec G306 finding on the public certificate's 0644 mode is annotated with its reason. Existing signing, reuse/reissue and SAN-splitting tests moved unchanged; new Run tests cover the required-flag and blank-SAN errors, an unreadable CA and the path output with IP and DNS SANs. Old and new binaries gave identical stdout, stderr, exit codes, file modes, CA verification, SANs and subject for 10 cases including SEC1 and PKCS8 CA keys, and the new binary reused the cached certificate and reissued it on a SAN change. Full preflight apart from the known vulnerability-scan finding passed.
ac069d8f3 cmd/eks-token-webhook (implementation) spinifex/agents/eks/tokenwebhook The control-plane VM's TokenReview webhook is EKS in-guest behaviour. cmd/eks-token-webhook keeps the -addr flag and loopback default, the environment-derived configuration (loadConfig, envOr, all eleven EKS_* variables, their default paths and the three required-variable errors, with its test), the slog text handler, both log lines and exit code 1; the agent exposes only Config and Run(ctx, Config) error and imports neither flag nor os.Getenv. The binary already returned errors from run, so no exit sites changed, and the certificate, kubeconfig, gateway-client and serve order is unchanged. Cancelling ctx now closes the server, which the binary never triggers. The relay, handler, certificate, kubeconfig and wire types moved unchanged with their tests; the public certificate's gosec G306 finding is annotated. New Run tests cover serving-cert, kubeconfig, gateway-client and listen failures and serving until cancelled. Old and new binaries matched for 12 cases including a live server's /healthz and /authenticate 405, 400 and 503 responses, file modes, kubeconfig body and log messages. Full preflight apart from the known vulnerability-scan finding passed.
3c82c69ad cmd/ecs-agent/{runtime,credentials} spinifex/agents/ecs/{runtime,credentials} The containerd runtime adapter and the instance-role credential provider are ECS-agent-private mechanism and move under the ECS agent tree unchanged, keeping their package names. cmd/ecs-agent is their only importer; its import paths and one comment changed. Both packages now fall under gosec, which reported nothing. ECS agent and subpackage tests passed 10 times under the race detector and in full preflight apart from the known vulnerability-scan finding.
59decbc25 cmd/ecs-agent (implementation) spinifex/agents/ecs/agent The ECS container-instance agent is ECS in-guest behaviour; package agent, imported by the binary as ecsagent. cmd/ecs-agent keeps process lifecycle only: the signal context, the separate New(cfg) and Run(ctx) calls, the ecs-agent: startup failed and ecs-agent: run failed log lines and exit code 1. Config, LoadConfig(envFile) and DefaultEnvFile stay in the agent as the guest-agent/AMI contract. The ldflags-set var version stays in the binary's main package so -X main.version keeps working, and reaches the register message through a new Config.AgentVersion field that LoadConfig never sets; a new test pins that New carries it into the identity. runtime and credentials remain ECS-agent-private subpackages. Moving out of cmd/ brought the code under gosec; its one finding, G101 on the mulga.ecs.credID container label key, is annotated as a key name. Old and new binaries built with the same -X main.version sent identical register bodies (including the injected version) to a fake TLS gateway with a fake IMDS, logged the same messages, exited the same on SIGTERM and failed identically with IMDS unreachable. Full preflight apart from the known vulnerability-scan finding passed.
55e858788 cmd/rds-agent (implementation) spinifex/agents/rds/agent The RDS guest agent is RDS in-guest behaviour and moves as one slice: its engines, commander, bootstrap handoff, quiesce, storage and parameter recovery share unexported types, so a finer split would need new exports. Package agent, imported by the binary as rdsagent. cmd/rds-agent keeps process lifecycle only: the signal context, Start(ctx, cfg) then Run(ctx), cancellation as a clean stop, the rds-agent: startup failed and rds-agent: run failed messages and exit code 1. The agent owns Config, DefaultEnvFile, LoadConfig with the engine-layout derivation, New, Start and Run; Start is the existing startup retry around New, kept as guest-agent readiness policy, and no generic retry helper is exported. The ldflags-set var version stays in the binary's main package and reaches the agent through Config.AgentVersion. Three image fixture paths gained one directory level and their regeneration hints name the new package; the fixture tests were confirmed to fail on the old paths. Moving out of cmd/ brought the code under errcheck and gosec, which reported no findings. Old and new binaries built with the same -X main.version produced the same startup retry messages, retry cadence, error text, stdout and SIGTERM exit status with no gateway URL and with a missing gateway CA; the post-registration path is covered by the moved unit tests. Full preflight apart from the known vulnerability-scan finding passed.
918150758 cmd/installer/{autoinstall,branding,firstboot,install,netprobe,systemd,ui} spinifex/operator/installer/<same> Installation, UI and autoinstall logic is operator delivery behaviour; the seven packages were already libraries and keep their names, and cmd/installer/main.go stays the thin executable with its TTY detection, messages and exit codes unchanged. The installer's systemd package writes the installed system's bridge, firstboot, banner and getty units, which is distinct from operator/host/systemd's Spinifex unit reconciliation, so the two are not merged. scripts/diff-coverage.sh's UI exclusion and a scripts/setup.sh comment follow the new path; the Makefile still builds cmd/installer/main.go. Moving out of cmd/ brought the code under gosec and errcheck for the first time. All 47 gosec findings are annotated per line with the artifact and why its mode is required: 0755 directories, executables and mount points, 0644 OS configuration, units, networkd files, fstab, GRUB assets and the public CA certificate, and 0444 machine-id, each stating that it contains no secret material; spinifex-firstboot.sh is annotated as never permitted to contain credentials or tokenised callback URLs, and the two G115 conversions are annotated as bounded Linux statfs and sysinfo values. The PSK-bearing WPA configuration was already written 0600 and the root password goes through chpasswd. The one errcheck finding, an unchecked *syscall.Stat_t assertion in the ownership check, is now a checked assertion returning a verify error instead of panicking. Installer tests and full preflight apart from the known vulnerability-scan finding passed.
05f390e61 cmd/spinifex/cmd spinifex/operator/cli The spx command tree is operator CLI assembly; package cmd becomes cli and cmd/spinifex/main.go stays the thin entry point calling cli.Execute(). The -X version and commit ldflags in the Makefile and build/Dockerfile.distro follow the new import path, and a build using the Makefile LDFLAGS printed the injected version rather than dev. Template paths in scripts/reset-dev-env.sh, migrate/VERSIONS.md and the NATS role test follow the move. The listener-bind invariant globbed the old template path, found nothing and passed without examining any template; it now uses the new path and fails when the glob is empty, confirmed against the old path. Moving out of cmd/ brought the tree under errcheck and gosec (105 findings uncapped). Config and flag wiring now goes through unexported mustBindEnv, mustBindPFlag and mustMarkFlagRequired helpers that panic with the config key, flag and command at startup instead of silently dropping a binding; all 110 sites are covered by the package's init and existing bind-function tests. Listing tables are written with Srender and a checked write because pterm's Render discards write errors; output is byte-identical and writing to a full device now exits 1. The formation-server shutdown discards are explicit, the http.DefaultTransport assertion is checked, twelve permission findings are annotated per line, and the formation join's InsecureSkipVerify is annotated as a known security gap (see dependency-rule debt). Full preflight apart from the known vulnerability-scan finding passed.
5032e24a7, d500e3802, e630703d7, eb68ec460, ebcafa01f gateway/ec2/{account,capacityreservation,key,launchtemplate,placementgroup} spinifex/domains/ec2/awsapi/<same> EC2 action adapters are domain-owned AWS API surface, so they move under the EC2 domain's awsapi, one commit per resource. Each package takes its directory name and callers use ec2<resource>api; moved files change only their package line. gateway/ec2.go keeps the dispatch map unchanged as the transitional registration point. Callers are the gateway dispatch and account teardown's EC2 reapers. Adapter, gateway dispatch and account-teardown tests, the integration and E2E builds, and full preflight passed.
c38bf99e4, 2ee60b20f, eaa5c971e, f6d8060f2, f50687210, 817e19bf2, 67992c8f5 gateway/ec2/{zone,vpc,eigw,eip,igw,natgw,routetable} spinifex/domains/ec2/awsapi/<same> The VPC-networking action adapters follow the same per-resource pattern; zone moves first because the instance adapter depends on it. Moved files change only their package line (vpc_test for its external tests). Callers are the gateway dispatch, account teardown's EC2 reapers, the quota live counters and the not-yet-moved instance adapter. Adapter, gateway and account-teardown tests, the integration and E2E builds, and full preflight passed.
4b315469f, 8b106e796, 7ee808d2d, 0485c77e6 gateway/ec2/{image,snapshot,volume,tags} spinifex/domains/ec2/awsapi/<same> Image, snapshot, volume and tag action adapters follow the same per-resource pattern; moved files change only their package line. Callers are the gateway dispatch, account teardown's EC2 reapers and the quota storage counter. The volume adapter still imports the instance adapter, which moves next. Adapter, gateway, account-teardown and quota tests, the integration and E2E builds, and full preflight passed.
b9a5eaf19, d4c68007e, 534c5b49d, 931af0ea5 gateway/ec2 (root), gateway/ec2/{idem,instance,spotinstance} spinifex/domains/ec2/awsapi, spinifex/domains/ec2/awsapi/<same> The root package holds EC2 resource-ARN resolution for authorization and becomes package awsapi (imported as ec2awsapi); the completeness test's failure messages now name its new path. The idempotency, instance and spot-instance adapters follow the per-resource pattern, instance and spot instance last because they carry cross-domain and runtime dependencies (see dependency-rule debt). Moved files change only their package line; docs/DESIGN.md examples use the new alias. gateway/ec2 no longer exists and gateway/ec2.go remains the transitional dispatch point. E2E suite selection is unchanged: neither the old nor the new path is in the manifest. EC2 domain, gateway, account-teardown, quota, daemon and awsgw tests, the integration and E2E builds, and full preflight passed.
cd9266176 gateway/restrouter.go spinifex/ingress/aws/rest The REST-JSON router maps method and path to an AWS action and percent-decodes path parameters because ARNs appear in paths, so it is AWS protocol mechanics rather than protocol-neutral HTTP and sits beside ingress/aws/query. The exported surface is Route[H], NewRouter and (*Router).Lookup; pattern parsing stays package-private and tested in place. The Bedrock and EKS route tables and their duplicate-route and completeness assertions stay in gateway, which computes the two former helper values inline rather than widening the API. Ingress and gateway tests, the integration and E2E builds, and full preflight passed.
ba2d59b29 gateway/ratelimit.go spinifex/ingress/aws/ratelimit Per-IP lockout after repeated failed SigV4 authentication returns AWS RequestLimitExceeded, so it is AWS ingress protection. Moved code changes only its package line and the four exports the gateway needs: MaxFailures, Fingerprint, TokenDigest and Anonymous; limiter behaviour is unchanged. gateway imports it as authlimit because it already imports Bluebottle's ratelimit throttler. The 14 implementation tests move with the package; the end-to-end gateway tests stay. The session-principal verdict and success-reset tests now read the record table through the lockout boundary instead of the limiter's private fields, and three mutations (success not clearing, double counting, legacy verdict recording) each fail them. Ingress and gateway tests, the integration and E2E builds, and full preflight passed.
— cmd/dhcptest, cmd/spx-loadgen unchanged Both already conform to ADR-0001's tooling row: the load engine is internal/testkit/loadgen and the DHCP probe's shared pieces come from the network domain's dhcp package. What remains in each main is its own flag parsing and one-off orchestration that nothing else imports, so extracting it would create a package solely to satisfy the directory map. No source change.
7047f6b36 — — ADR-0002 phase 0: characterization tests pin the ECR control plane as it behaves today before dispatch moves. Full-pipeline tests fix the 400/501 error envelopes, JSON 1.1 content type and X-Amzn-Errortype, 413 before dispatch for an oversized body, 503 for stub and composed actions when NATS is down, and authorization before a composed action. Unit tests fix target parsing (dispatch and telemetry agree on every malformed form), the stub and inventory sets, GetAuthorizationToken ignoring body-supplied identity, and the action reaching the throttle key, trace span and audit record. Mutation of the target parser, the authorization gate and the audit action each failed the new tests. Test-only; full preflight passed.
89364c100 — spinifex/ingress/aws/dispatch, spinifex/ingress/aws/invariants ADR-0002 phase 1: the registration contract. A startup Builder validates and seals registrations into an immutable Registry keyed by credential-scope service, with aliases, an inventory name, an advisory action resolver, the dispatcher, the error envelope and the operation inventory; Invocation carries the bounded request, account, Region, an AuthorizeFunc and a lazy ActorFunc. It imports only foundation/aws/errors and Bluebottle's iampolicy condition-key vocabulary, and no domain moved. TestS4_IngressAWSImportsNoDomainOrRuntimeImplementation binds ADR-0002 S4 for all of ingress/aws and failed on an injected domain import. Contract tests at 100% statement coverage under -race; full preflight passed.
86a2de3ff spinifex/gateway (dispatch switch) spinifex/gateway consuming spinifex/ingress/aws/dispatch ADR-0002 phase 2: the gateway consumes the registry with no service registered. A registered service passes the auth gate, resolves its own action label, renders errors in its declared envelope through one gw.jsonErrorService method, and is dispatched with an Invocation whose Authorize calls the existing policy check and whose lazy Actor reuses buildCallerARN. The NATS-unavailable guard still runs ahead of both paths; every existing service takes the unchanged switch, now named the legacy path that must only shrink. The awsgw role wires an empty registry and fails startup on any overlap with the legacy table. Fake JSON, XML and resolver-less registrations prove dispatch, telemetry labels, authorisation deny/allow, the verified actor, both envelopes, the 503 and unknown-service paths and the overlap guard through the full pipeline; four production mutations were each caught. Phase 0 ECR characterisation and the unserved-service tests passed unchanged; full preflight passed.
6c48ab6b7 spinifex/gateway (ECR_Request, ecrActionFromTarget, ecrAuthorizationPrincipal, the four ECR* GatewayConfig fields, the ecr legacy table/switch entries) spinifex/domains/ecr/awsapi (NewRegistration, OperationInventory), wired by spinifex/runtime/roles/awsgw ADR-0002 phase 3: ECR's AWS JSON 1.1 control plane registers from its domain. The dispatcher reproduces the removed gateway path step for step (error codes and order, one bounded body read feeding scope and invocation, Authorize before every use case, nil capability as ServerInternal, the token principal from the verified Actor, the JSON writer). The gateway no longer imports domains/ecr/awsapi; the OCI /v2 route is unchanged. Operation coverage reads the pure OperationInventory() that the registration uses verbatim, proved equal by TestRegistrationInventoryIsTheDeclaredInventory; coverage.json and every coverage page were byte-identical. TestS4_OnlyAWSGWRoleImportsDispatchAndECRAWSAPI binds acceptance item 3, with cmd/aws-model-coverage as the only named, non-serving tool exception. Five mutations (authorise after use, prefix-strict resolver, inventory drop, body-derived token principal, probe import) were each caught; Phase 0 characterisation kept every assertion except one rebound from the deleted parser to the registration's resolver; full preflight passed. Remaining temporary edge: Deps.NATS is a raw *nats.Conn.
5f66bd2a3 spinifex/gateway (GenerateEC2ErrorResponse, GenerateIAMErrorResponse, GenerateS3ErrorResponse, requestSignalsJSONProtocol, jsonErrorType, error content types), spinifex/gateway/eks (GenerateEKSErrorResponse) spinifex/ingress/aws/envelope ADR-0002 phase 4, error rendering: pure EC2, IAM-style and S3 XML builders, the AWS JSON 1.1 body and error type, the header-only JSON-protocol detector and content types, importing only the standard library. ErrorHandler, the cluster-unavailable, throttle and SigV4 writers and the legacy service-to-envelope selection stay in gateway. A 140-case golden of status, headers and body across 14 service cases and every error writer was identical before and after; two mutations were caught; the S4 ingress import invariant scans the new package; full preflight passed.
b81ae4eb4 spinifex/gateway (service served/action/error-protocol selection, overlap check) spinifex/ingress/aws/dispatch (Selector, Legacy) ADR-0002 phase 4 slice 2: one selector decides whether a service is served, its throttle/trace/audit action, its error protocol, its registered entry and the startup overlap; a registered service always wins and a resolver-less registration does not fall back to legacy action resolution. The gateway supplies its legacy tables through the three-method dispatch.Legacy interface, so ingress imports nothing from gateway; legacy dispatch, the Bedrock sub-service mapping and invocation building (which read gateway request-context keys) stay in gateway. A 160-case golden capture through the real SigV4, trace and Request chain with live test NATS and the ECR registration was identical before and after; mutating Served, ResolveAction and Validate each failed tests. Only startup-visible change: the overlap error now begins dispatch: and names the first overlap in sorted order. Full preflight passed.
4b0a4dd6b — — ACM characterization before registration: full-pipeline 400 MissingAction/InvalidAction envelopes, a 200 ListCertificates against a live NATS responder, dispatch and telemetry target-parser agreement, authorization before the NATS call (subject counter stays zero), 413 before dispatch, 503 when NATS is down (ACM's own nil-connection check is unreachable over HTTP), the action reaching throttle, trace and audit, and the nine-action inventory. Mutating the parser, the authorization call and the action table each failed tests. Test-only; full preflight passed.
fdaf589ca spinifex/gateway (ACM_Request, acmActions, acmActionFromTarget, legacy acm entries) spinifex/domains/acm/awsapi (NewRegistration, OperationInventory) ACM registers its AWS JSON 1.1 control plane from its domain, wired only by the awsgw role; the dispatcher reproduces the old order (missing/unknown action, InternalError without an account, bounded body, resource ARNs, authorization, then nil-NATS and the action). The S4 composition invariant now covers every registered domain awsapi with the same named coverage-tool exception. Coverage output was byte-identical and the ACM integration conformance summary was identical at the parent and after. Mutating authorization order, the target parser, the inventory and gateway imports each failed tests. Temporary edge: Deps.NATS is a raw *nats.Conn because every ACM action function takes one. Full preflight passed.
01276bf29 — — RDS DB subnet group characterization before extraction (ADR-0003 S1, ADR-0004): repeat delete reports not found, name scoped to the account, modify never moves a placed instance, snapshots do not block delete, restore rejects an unknown group, tags reached through the ARN and gone with the group, persisted record field names. Present contract and gaps in docs/package-boundary/rds-db-subnet-group-lifecycle.md; no conformance claimed.
17dc29251 spinifex/handlers/rds (DBSubnetGroupRecord, DBSubnetGroupSubnet, DBSubnetGroupKey, DBSubnetGroupsPrefix, getDBSubnetGroup, resolveGroupSubnets and the create/modify/delete bodies) spinifex/domains/rds/subnetgroup (Owner, Record, Subnet, Key, Prefix) First ADR-0004 resource owner. The handler keeps AWS validation, projection and paging; delete reaches instances through the owner-declared Dependants, and instance placement reaches the group through the instance-declared subnetGroupPlacement. Stored shape unchanged; docs/service-interfaces.yaml lists the package with the RDS daemon. No conformance claimed; gaps in the lifecycle contract.
1cb868e68 — — ECS task definition characterization before extraction (ADR-0003 S1, ADR-0004): stored keys and field names, every reference form and its not-found fault, repeatable deregister, an inactive revision still launching, bare-family launch resolution, a service pinning its resolved revision. Present contract and gaps, including the bare-family iam:PassRole re-resolution security gap, in docs/package-boundary/ecs-task-definition-lifecycle.md; no conformance claimed.
544b0c6a0 spinifex/handlers/ecs (TaskDefRecord, ContainerDef, TaskDefRevKey, TaskDefLatestRevKey, ParseTaskDefRef, TaskDefARN, TaskDefRefARN, revision register/deregister/resolve/list) spinifex/domains/ecs/taskdefinition (Owner, Record, Container, PortMapping, SystemControl, ParseRef, ARN, RefARN, keys) ADR-0004 owner extraction (ADR-0003 S1 contract in docs/package-boundary/ecs-task-definition-lifecycle.md). Stored keys and JSON field names unchanged; the owner declares its own persisted PortMapping/SystemControl so it does not import handlers/ecs/bus. Launches use the launch-side taskDefinitionResolver; gateway authz and all iam:PassRole checks unchanged. docs/service-interfaces.yaml now lists spinifex/handlers/ecs and the new package under spinifex_daemon. Proves ownership and dependency direction only; no conformance claimed.
b36d70b22 — — EKS access entry characterization before extraction (ADR-0003 S1, ADR-0004): persisted and nested field names, pre-existing JSON decodes, actions ignore cluster status, associate without an entry is not found, disassociating an unbound policy is a no-op, an empty update keeps username and groups, cluster delete sweeps only that cluster's entries, token review needs no cluster record. Present contract and gaps in docs/package-boundary/eks-access-entry-lifecycle.md; no conformance claimed.
68ee6f620 spinifex/handlers/eks (AccessEntryRecord, AssociatedAccessPolicy, AccessScope, AccessEntriesPrefix, AccessEntryKey, PrincipalARNHash, access-entry KV helpers, policy catalogue, effectiveGroups, the access mutation logic) spinifex/domains/eks/access (Owner, Record, AssociatedPolicy, Scope, Spec, Get, List, Key, Prefix, PrincipalARNHash, SupportedPolicies, EffectiveGroups) ADR-0004 owner extraction (ADR-0003 S1 contract in docs/package-boundary/eks-access-entry-lifecycle.md). Stored keys and JSON field names unchanged; writes go only through Owner, including the creator-admin seed at cluster launch. The handler keeps AWS validation, the cluster existence check, error mapping and projection; gateway authz switches only its hash import. docs/service-interfaces.yaml now lists spinifex/handlers/eks and the new package under spinifex_daemon. Proves ownership and dependency direction only; no conformance claimed.
955486421 — — RDS engine catalogue characterization before extraction (ADR-0004): family normalisation, exact error codes, per-parameter agent metadata, internal-fault outcomes, recovery text, reference class. Static metadata, so no ADR-0003 lifecycle contract; boundary and evidence note in docs/package-boundary/rds-engine-catalogue-boundary.md.
f1f194b16 — — Discovered and corrected during the catalogue characterization: a missing MasterUserPassword on CreateDBInstance returned HTTP 500 ServerInternal; it now returns 400 InvalidParameterValue, covered through the gateway by gateway/rds_test.go TestRDSRequest_CreateDBInstance_EmptyMasterUserPasswordReturns400.
4b5524781 spinifex/handlers/rds (Engine, engine registry and lookup, family mapping, version/username/DB-name validation, postgres/mariadb catalogues, ParameterSpec, ApplyType*, ParamType*, ResolveEffectiveParameters, db.* classes, engine-version and orderable selection) spinifex/domains/rds/engine (Engine, LookupEngine, EngineForFamily, ParameterSpec, Setting, InstanceTypes, Sizing, NewSizing, EngineVersions, OrderableOptions) ADR-0004 owner move of static metadata; boundary note in docs/package-boundary/rds-engine-catalogue-boundary.md. Two deliberate boundaries: EC2 instance-type memory is consumed only through Sizing, never by a direct RDS to EC2 package dependency; agent engine metadata is consumed only through the agent-declared EngineCatalog contract composed in cmd/rds-agent, never by importing the catalogue. Effective parameters cross as engine-owned Setting, converted to Parameter in handlers/rds. AWS output, paging, apply-method mapping, storage limits and the runnable check stay in the adapter path.
ca1dd9aa9 — — RDS DB parameter group characterization before extraction (ADR-0003 S1, ADR-0004): persisted meta and override field names, pre-existing JSON decodes, default groups synthesised and never stored, repeated delete is not found, a pending attachment blocks delete, a dynamic pending-reboot override is still applied live, propagation reaches a stopped instance. Present contract and gaps in docs/package-boundary/rds-db-parameter-group-lifecycle.md; propagation to instances is recorded as instance-side realization outside the group, and no conformance is claimed.
d9c3f3ca6 spinifex/handlers/rds (DBParameterGroupRecord, DBParameterRecord, the parameter-group key helpers, ListDBParameterGroupNames, ListDBParameterOverrides, getDBParameterGroup and the create/modify/delete writes) spinifex/domains/rds/parametergroup (Owner, Record, Override, MetaKey, ParamsPrefix, ParamKey, Prefix) ADR-0004 owner extraction (contract in docs/package-boundary/rds-db-parameter-group-lifecycle.md). Stored keys and JSON field names unchanged. Delete reaches instances through the owner-declared Dependants; instance-side resolution reads through the instance-declared parameterGroupValues. SetOverrides refuses an unknown group. The handler keeps AWS validation (it refuses every default.* name; the owner refuses only a real engine default), projection, paging and propagation. Proves ownership and dependency direction only; no conformance claimed.
52de5e517 — — EKS add-on characterization before extraction (ADR-0003 S1, ADR-0004): persisted record and manifest keys and field names, durable CREATING and staged descriptor, installer failure as CREATE_FAILED (update too), update returning Successful while UPDATING, mismatched-version reports accepted, delete erasing both keys while answering DELETING, the ignored client token, no PassRole check on serviceAccountRoleArn, and the exact internal add-ons body (Go field names, not the declared json tags) and agent TSV. Present contract and gaps in docs/package-boundary/eks-addon-lifecycle.md; no conformance claimed.
d89963054 spinifex/handlers/eks (addon_catalog.go, addon_state.go, addon_installer.go, the add-on key helpers, the create/update/delete transitions, markAddonFailed, nextAddonStatus and report application) spinifex/domains/eks/addon (Owner, Record, Manifest, Status, Spec, Installer, StagingInstaller, Report, Key, ManifestKey, Prefix) ADR-0004 owner extraction (contract in docs/package-boundary/eks-addon-lifecycle.md). The owner is the sole writer; stored keys and JSON fields unchanged. The handler checks cluster existence (acctKVForCluster) and keeps AWS validation, error mapping and projection. ClusterReconciler remains the subscriber host and delegates through the reconciler-declared addonReports; node groups call EnsureGPUDevicePlugin. Guest wire types stay in handlers/eks. Behaviour preserving: proves ownership and dependency direction only; every lifecycle gap remains.
692d929b4 spinifex/handlers/eks (AddonStatusSubject, AddonDeliveryPhase, AddonStatusReport, StagedAddonManifest), gateway/eks (internalAddonsOutput), agents/eks/gatewayfetch (stagedAddon, internalAddonsResponse) contracts/eks/v1 The add-on guest/controller wire is an explicit versioned contract; v1 is the deployed form and a generation-aware protocol needs a successor version. Bytes unchanged: the internal add-ons body is rendered by jsonutil.BuildJSON with Go field names, which the contract documents and pins. Compatibility tests cover the status report, the guest printf form, the manifest, both body forms and the TSV. The guest script still builds report JSON by hand without escaping.
4e822fa34 — — EKS add-on v1 compatibility characterization: pins which owner (domains/eks/addon) writes drop unknown record and manifest fields, that reads and deletes ignore or erase them, that one undecodable stored entry fails a listing rather than the whole set, and that contracts/eks/v1 and agents/eks/gatewayfetch wire consumers ignore extra fields. Test-only; no conformance claimed.
7e504c1dd — — Stale paths left by the moves: .gitignore un-ignores the moved runtime/roles/spinifexui/frontend/dist/, UI CI and the Dependabot npm job point at the moved frontend, the depguard ec2-control-plane rule covers domains/** as well as handlers/**, and CODEOWNERS names the network-layer proposal rather than an accepted ADR.
d5849c6b3 spinifex/handlers/quota spinifex/domains/admission/quota ADR-0001's handlers/quota current-to-target map row. Package renamed to quota (imported as admissionquota where a local quota identifier exists); KV buckets, subjects and error codes unchanged. Its existing debt edges moved with it and are recorded below; runQuotaReconcile stays in runtime/roles/awsgw. Full preflight passed apart from the recorded Go 1.27.0 advisory exception.
8cd9cff75 spinifex/migrate/migrate_test.go (the Registry/RunKV tests and their NATS helpers) spinifex/foundation/state/migrate/kv_test.go The KV migration engine's only tests lived in the operator upgrade package's test file. They move to the package that owns the engine before the workflow moves, so operator/upgrade does not hold another package's test suite. No test name, assertion or behaviour changed.
919d5bfa6 spinifex/migrate spinifex/operator/upgrade Owner decision: the operator-driven config-file and object-store upgrade workflow behind spx admin upgrade belongs to operator/upgrade, distinct from foundation/state/migrate. Package renamed to upgrade; no registry types, migration names, versions, file formats or command names changed. It imports only foundation/state/{kvutil,migrate}, providers/objectstore and jetstream, so it carries no layering debt. scripts/diff-coverage.sh and VERSIONS.md point at the new path. Full preflight passed apart from the recorded Go 1.27.0 advisory exception.
5292bd22c — — ADR-0001 layering ratchet added, with the nine import edges it found missing from this record recorded below; it lives at architecture/layering beside tests/, outside the product tree.
8bf162fb7 spinifex/utils/ec2_subject.go (SubjectEnsureDefaultVpc) contracts/ec2/v1 (EnsureDefaultVpcSubject) The default-VPC request/reply subject crosses the gateway–daemon process boundary, so its home is the EC2 contract. Only the subject moved; the request and reply bodies remain anonymous structs on each side, as the contract README states.
6b01f39e7 spinifex/admin (config generation, certificate issuance, host discovery, telemetry, email validation, image import/removal, orphan volumes) spinifex/operator/admin These are operator use cases consumed only by operator/cli and operator/installer/ui. Certificate issuance moves as an operator use case; foundation/pki keeps generic CA-key loading and the tenant ACM CA stays in domains/acm. Legacy admin keeps DefaultAccountID/DefaultAccountName, GenerateAWSAccessKey/GenerateAWSSecretKey and images_promote.go; the private readAMI helper and the removal test fixtures are duplicated there rather than shared. No forwarding shim; operator/cli imports the legacy package as legacyadmin. Full preflight (excluding the recorded Go 1.27.0 advisories) passed.
29f9ad07f (characterisation only) — Pins the ebs.<node>.fenced and ebs.<node>.abandon subjects (named and single-node forms), the exact JSON of VolumeFencedEvent, VolumeAbandonRequest/Response and DirtyRecord, the VIPERBLOCK_VOLUME_DIRTY bucket name and the volume key rule against golden literals before anything moved.
53697bc76 spinifex/services/viperblockd/vbwire (fence and abandon subjects and payloads, DirtyBucket, key rule, DirtyRecord) contracts/viperblockd/legacy/v1 (fence.go, abandon.go, dirty_marker.go) These routes and the dirty marker cross the daemon–viperblockd process boundary and, for the marker, persist across nodes, so they are deployed legacy contract vocabulary. The characterisation tests moved with them, literals unchanged. The README records the approved dirty-marker boundary: the adapter owns it, only the adapter writes, recovery on any node and spx admin (read-only) consume it, no TTL, N/N-1, no renames or reinterpretation, and a breaking change needs a successor contract. docs/service-interfaces.yaml now lists both routes for the daemon and viperblockd.
f17e93aed spinifex/services/viperblockd (lease, fence, dirty-marker writes, nbdkit recovery, provider handlers, Config) and vbwire I/O helpers spinifex/providers/ebs/viperblock and spinifex/providers/ebs/viperblock/dirty The Viperblock EBS adapter is a provider; it and internal/testkit/vbscan are the only importers of the Viperblock library. dirty exposes only ListUnsealedVolumes; it never creates the bucket (unchanged behaviour), and write, clear and purge stay unexported in the adapter. operator/cli uses the reader, so it is not a raw KV client. vbwire is deleted and its frozen-list and allowlist entries removed. The adapter loads its key through bluebottle/pkg/masterkey, so utils/encryption.go was not needed.
7e70222f1 spinifex/services/viperblockd (role supervision, opt-in live data-path tests) spinifex/runtime/roles/viperblockd ADR-0001 maps services/* to runtime/roles/*; what remained is an 82-line role. It was the last package under services/, which now holds nothing. Full preflight (excluding the recorded Go 1.27.0 advisories) and manifest-check passed for each of the four commits.
7a45e7a3e (characterisation only) — Pins the subject, JSON, reply envelope, delivery mode and queue group of all 22 inbound vpc.* network-realisation routes where they lived. docs/service-interfaces.yaml gains six live routes it was missing (IGW routes, subnet egress gating, system egress).
77ac0f0f7 (new contract) contracts/network/v1 Versioned vocabulary for the vpc.* control-plane-to-network-realisation boundary: one file per route family, the {success,error} acknowledgement envelope and the vpcd-workers queue group. Network owns the meaning and wire contract because it owns OVN and host realisation; EC2 and managed-service domains own their AWS-visible transitions and decide when to project them. The README states it does not move vpcd and changes no lifecycle or reliability semantics.
105048594 Subjects and payload types in domains/network/subscribers, domains/ec2/{vpc,eip,natgw}, handlers/eks and contracts/ec2/v1/igw.go contracts/network/v1 Producers and the subscribers use one vocabulary; the duplicates are deleted, including the IGW attach/detach declarations in contracts/ec2/v1 (network is the sole owner). Wire bytes are unchanged: networkv1.SecurityGroupRule carries the publisher's nine fields with its tags (the subscriber-side golden now pins the "rule_id" the publisher always sent), and both NAT-gateway publisher shapes marshal as before. domains/ec2/vpc.SGRule stays because it is EC2's persisted rule record, converted at the publish boundary.
3aa82953e, 46f448963 spinifex/utils/vpcd_event.go and the raw vpc.* publishes in EC2, ELBv2, EKS, the daemon and domains/network/systemvpc spinifex/domains/network/projection A typed projection client with one method per operation and delivery mode; callers pass no subject strings. Timeouts (5s, 45s NAT add, 15s NAT delete), the NAT add barrier, the ErrNoResponders retry (3 attempts, 500ms) and non-fatal teardown are unchanged, and the inconsistent NAT paths keep their current mode under honest names (recorded below). The IGW, NAT-gateway, IGW-route and egress-gate publishes previously logged their own WarnContext messages with subnetId; they now log the shared publish warning without request context or subnet, a diagnostic loss only.
fd88e8ceb spinifex/utils/encryption.go (LoadViperblockMasterKey) spinifex/domains/ec2/ebs/encryption (Enabled) EC2's AWS-visible EBS encryption posture, a temporary reading of the provider's key-file setting rather than an encryption implementation. Cache, error text and each caller's handling are unchanged (image reports unencrypted on a load failure; instance and volume return InternalError). utils/ is deleted; the layering test retires utils and services and fails if either regains files. The S7 subject-prefix scan now walks contracts/network, where the vpc.* literals live; after the projection move it had silently stopped listing them. Full preflight (excluding the recorded Go 1.27.0 advisories) and manifest-check passed for each commit.
38fbd1499 (characterisation only) — ADR-0007 slice 0: pins the local file's exact bytes and schema failure modes, the instance bucket configuration, the i.<id> key and wire form, the bucket version gate, generation preservation across every KV writer and the VM conversion, and what a returning node does with a missing, foreign, terminal or unreadable canonical record. Current behaviour, not the ADR target; see the slice 0 section below.
6fa09d336 (governance only) — ADR-0007 slice 0 ratchet at architecture/instancestate: freezes the 73 direct production uses of raw instance state found by the inventory below.
2a196f9ba (governance only) — ADR-0007 slice 0 ratchet narrowed to instance-state access: the JetStreamManager type and constructor are no longer matched, the cluster-state consumers INV-09 and INV-19 leave the allowlist, and 67 entries remain.
09646388e (revert only) — The ADR-0007 slice 1a design and slice 1b step 1 codec stack (e9e11d531 to c3f302423) leaves this ADR-0001 checkpoint: no production reader or writer called the codec, so carrying it would ship dormant lifecycle implementation. record_codec.go, its tests and docs/package-boundary/instance-state-transition-record-design.md are removed, with the codec's instance-state ratchet owner-file line and the record rows and section describing them. The slice 0 evidence (38fbd1499, 6fa09d336, c04222b27, 2a196f9ba, 830f70489) stays, and domains/ec2/instance/state stays for the vpcd IMDS readers. The stack is reapplied on a fresh ADR-0007 branch from dev after this checkpoint merges.
e40e699e5 spinifex/vpcd/imds_instance_state.go spinifex/domains/ec2/instance/state/imds_readers.go The local-file reader (LocalVMReader/NewLocalVMReader, formerly localVMStateReader) and the instance-record loader opener (OpenInstanceRecordLoader, formerly newInstanceRecordLoader) move unchanged to the instance-state boundary; vpcd.Config gains LocalVMs guestmetadata.LocalStateReader and OpenInstanceRecords func(ctx, nc) (guestmetadata.RecordLoader, error), wired by operator/cli with state.NewLocalVMReader(nodeConfig.DataDir) and state.OpenInstanceRecordLoader, so vpcd no longer imports daemon directly. guestmetadata's own narrow interfaces are exported (LocalStateReader, RecordLoader) with no semantic change so vpcd.Config can name them. The opener's absence is now a fatal startup error rather than an unchecked call, matching the existing mandatory-open behaviour. The instance-state ratchet's INV-13 entries move with the file; OpenInstanceRecordLoader returning guestmetadata.RecordLoader directly (so the two packages' function types match exactly) removes the file's local recordLoader interface and its vm.InstanceRecord reference, so INV-13 now cites 5 symbols rather than 6. vpcd still runs the legacy bucket-opening path (InitKVBucket, the version gate and the instance migrations) through the injected opener; that is ADR-0007 debt (INV-13) and is deliberately not fixed here. Full preflight passed except the recorded Go 1.27.0 advisories.
7bf6c10d7 spinifex/vpcd spinifex/runtime/roles/vpcd The whole vpcd role moves unchanged to its ADR-0001 runtime-role target: all sixteen production and test files (vpcd.go, the lease reaper, the OVN controller watchdog, the host-EIP loop, lease datapath reconcile and their tests), with no split and no lifecycle-behaviour change. The package name stays vpcd. operator/cli/service.go and runtime/service/{service,service_test}.go update their import path. docs/service-interfaces.yaml's vpcd service entry, .github/CODEOWNERS and the real-manifest case in tests/e2e/select-suites/selector_test.go follow the new path; that package's own synthetic testManifest() fixture keeps its illustrative spinifex/vpcd string by design, decoupled from the real manifest. The network invariant scan roots in domains/network/invariants/{nat_mode,nats_subjects,names}_test.go and the S3 exemption for vpcd.go follow the new path; injected UplinkMode() and vpc--prefixed concat faults in moved files failed S3 and S4 respectively, confirming the scan still covers them, and were reverted by hand. ingress/aws/invariants/deps_test.go keeps vpcd in its forbiddenRoots as defence in depth: runtime already rejects an import of the new path, confirmed by an injected import of runtime/roles/vpcd into an ingress/aws file, and the retained entry still rejects an ingress import of a recreated legacy vpcd directly. The layering package drops vpcd from legacyRoots, runtimeLegacy and frozenLegacyPackages and adds it to retiredLegacyRoots, so runtime/roles/vpcd classifies as runtime automatically and a revived spinifex/vpcd directory fails the frozen-root guard, both confirmed by fault injection and reverted; the stale operator/cli → vpcd allowlist entry is deleted rather than repointed, because operator already imports other runtime/roles/* packages with no allowlist entry and no rule forbids it. The instance-state ratchet's allowlist cites no vpcd path and is unchanged.
a130f6b8e spinifex/handlers/ecs/bus contracts/ecs/v1 (ecsv1) The ECS scheduler-agent wire vocabulary moves with no legacy shim: five subject builders and the message types (RegisterInstance, Heartbeat, Assign, AssignContainer, StopDirective, TaskState, ContainerStatus, InstanceCapacity, PortMapping, SystemControl, status constants). Subject strings, JSON fields and types are byte-identical; the old-agent AssignContainer compatibility test moves unchanged and new golden pins cover every message type that had none. A trace found only service-reconcile live on the bus (handlers/ecs/instances.go publishes, the scheduler leader subscribes); the agent reuses Assign, StopDirective and TaskState as payloads of the gateway's PollAssignments and SubmitTaskStateChange actions, and the README route table and type comments say so. docs/service-interfaces.yaml lists ecs.bus.* and the contract path under the daemon. The layering package drops the stale agents/ecs/agent → handlers/ecs/bus allowlist entry and the frozen-package entry.
263c58167 spinifex/admin (images_promote.go: SystemOwnerAlias, PromoteImageOpts/PromoteImageResult, PromoteSystemImage, GetAMIMetadata, the private readPromotedSnapshot, and its test fixtures) spinifex/domains/ec2/image The cohesive image-promotion API moves to its existing domain owner with only the package line changed: no storage, key, ordering, error or promotion-behaviour change, and no name collisions. daemon/daemon_handlers_image.go and gateway/spx/promote_image.go drop their admin import and use ec2image; operator/cli/admin.go adds ec2image and keeps legacyadmin for the account identifiers and key generators. admin now holds only accounts.go. No new forbidden edge and no stale allowlist entry.
3fdaa54d9 spinifex/admin (DefaultAccountID, DefaultAccountName) spinifex/foundation/aws/identifiers The fixed, platform-reserved bootstrap account identifiers join GlobalAccountID: the ID and name of the first human-facing account created during bootstrap, used by daemon, gateway, runtime/roles/awsgw, accountteardown and operator/cli. Function names and values are unchanged, and they stay fixed rather than configurable. Callers use the existing awsidentifiers alias; daemon, gateway, runtime/roles/awsgw, accountteardown and the other operator/cli files drop their admin import, and operator/cli/admin.go keeps legacyadmin only for the credential generators. The TestDefaultAccountID assertion moves with the function, plus a name pin and a check that the ID is valid and distinct from GlobalAccountID. admin now holds only the IAM credential generators.
254d898a3 spinifex/handlers/sts (oidc_jwks.go: the handlers_eks.AccountBucketName/OIDCJWKSKey import) spinifex/handlers/eks (OIDCJWKSReader, a ClusterJWKSResolver implementation), wired by spinifex/runtime/roles/awsgw The Q-22 STS-to-EKS dependency is cut by a consumer-owned resolver: handlers/sts declares ClusterJWKSResolver (raw-bytes lookup, the same found/error split FetchClusterJWKS already had), and handlers/eks implements it beside AccountBucketName/OIDCJWKSKey with the same bucket-open-then-get, timeouts and not-found classification oidc_jwks.go used. STS still decodes the JWKS, applies the empty-keys rule and maps every outcome to InvalidIdentityToken, unchanged. runtime/roles/awsgw and tests/integration/harness.go construct the resolver and pass it to NewSTSServiceImpl, now a required, nil-checked argument; the compile-time interface check lives in awsgw because handlers/eks must not import handlers/sts. The literal bucket/key, KV-dependency-failure and cluster-status-not-consulted characterisation tests move to handlers/eks, next to the layout they pin; every remaining handlers/sts test, including the web-identity fixtures and newTestSetup, is rewritten against a test-owned fakeJWKSResolver and imports no EKS package. The one test proving an EKS-minted issuer URL parses through STS's ParseEKSIssuerURL moves to tests/integration as a composition test. The stale handlers/sts → handlers/eks allowlist entry is deleted.
0368e861a spinifex/admin (accounts.go: GenerateAWSAccessKey, GenerateAWSSecretKey, and their format and uniqueness tests) spinifex/foundation/aws/credentials (awscredentials) A stateless AWS credential-format primitive: it generates an AKIA + 16-alphanumeric access key ID and a 40-character base64 secret key, and issues, stores and authorises nothing. It is not IAM issuance and does not implement ADR-0005 S5. Output, randomness source, errors and names are unchanged; only the package line and doc comment changed. It is deliberately not unified with handlers/iam's internal generateAccessKeyID, whose IDs are AKIA + 20 uppercase hex characters. handlers/iam and operator/cli/admin.go import awscredentials; operator/cli no longer imports any legacyadmin. The spinifex/admin directory is deleted: admin leaves the legacy-root, frozen-package and operator-legacy lists, joins the retired roots so it cannot return, and its two allowlist entries (handlers/iam → admin, operator/cli → admin) are removed. Operator bootstrap still generates the admin and Northstar credentials directly, recorded as debt below.
The EC2 contract row is intentionally different from the directory moves: it creates
a compatibility boundary. ec2.cmd.* retains its deployed one-token NATS
subscription and permission shape, while callers use
InstanceCommandSubject rather than reconstructing the subject. The
contract's README states the route, scope and change rule.

ECR composition boundary remaining

The ECR AWS JSON dispatcher and /v2/* route assembly remain methods on the generic GatewayConfig. They establish the generic HTTP route, authenticated request context, bounded-body read and policy gate, then invoke the ECR domain packages above. They are not moved merely to remove the last ECR-named files from gateway: doing so first would make the ECR domain own generic gateway state and policy mechanics.

Registry-backed image actions have now crossed this boundary through awsapi.RegistryActionService: the composition root supplies the OCI capabilities, and the generic gateway dispatches through it after authorization.

Lifecycle-preview actions have likewise crossed it through awsapi.LifecyclePreviewActionService, whose policy-store-plus-image-catalog dependencies remain explicit and separate from the registry image-action service.

Repository metadata actions have crossed it through awsapi.RepositoryActionService, which makes both their narrow metadata-store dependency and advertised endpoint profile composition explicit.

Authorization-token issuance has crossed it through awsapi.AuthorizationTokenActionService. Gateway intentionally still builds the canonical IAM/STS principal before dispatch, because ECR must not depend on gateway identity context or ARN construction.

All implemented ECR control-plane actions now dispatch through explicit ECR capabilities. The remaining ECR-named gateway code is generic /v2/* route assembly, SigV4/registry authentication and policy enforcement; it remains in gateway rather than inverting those dependencies into the ECR domain.

ACM relocation residuals

The ACM move leaves two pre-existing dependency seams visible rather than changing their behaviour in a structural slice:

  • domains/acm.Store still imports IAM's AES-GCM helper functions. It does not read IAM records, but the crypto mechanism must move behind a shared crypto boundary before the final domain-dependency lint can prohibit that implementation import.

The ACM action-name dispatch map has since cleared: fdaf589ca below registers ACM's AWS JSON 1.1 control plane from domains/acm/awsapi, wired only by runtime/roles/awsgw, and no ACM_Request or acmActions/acmActionFromTarget dispatch remains in gateway.

Guest metadata relocation residuals

The guest-metadata move keeps every existing import unchanged, so the domain package now shows these breaks of the target dependency rules. Each needs a consumer-owned capability, projection or versioned contract before the dependency lint (ADR step 8) can be enabled.

  • domains/ec2/guestmetadata imports the DNS, IAM and STS implementations, which breaks rule 3 (no domain reads another domain's implementation). Its EC2 key and instance imports are same-domain once those resources sit under domains/ec2; they remain resource-to-resource implementation imports for the EC2 decomposition to review.
  • domains/ec2/guestmetadata imports runtime/compute/vm for instance records and VM reconstruction, the first domain-to-runtime edge; runtime is meant to compose domains, not be imported by them.

Dependency-rule debt to clear after the moves

These residuals were recorded by individual slices and must be resolved once the structural moves are complete, before the dependency lint is enabled.

  • Guest metadata: the cross-domain and runtime imports above.
  • EC2 → network implementation: domains/ec2/vpc/eni.go:18 and eni_lease_rebind.go:11 import domains/network/topology, eni.go:17 also imports domains/network/identifiers, external_ipam.go:10-11 imports domains/network/external and domains/network/external/dhcp, domains/ec2/eip/service_impl.go:17 and lease_rebind.go:11 import domains/network/topology, and domains/ec2/instance/service_impl.go:31 imports domains/network/topology. EC2 reads network's implementation directly rather than through a capability or projection; this clears when EC2's network-facing reads and writes go through the EC2-owned projection recorded above (step 7).
  • Cross-domain: domains/ecr/auth/signingkey.go:24 and domains/ochre/vector/appliance.go:15 import handlers/iam; this needs an IAM capability rather than the IAM implementation package. domains/ochre/launch.go:17-18 and domains/ochre/vpc.go:9 import domains/ec2/systeminstance and domains/network/systemvpc; Ochre consumes managed-service launch and system-VPC realisation as domain implementations rather than capabilities.
  • Domain → runtime: domains/ochre/capacity.go:9 imports runtime/compute/gpu; domains/ec2/volume/service_impl.go:30 and volume_leak_reaper.go:8 import runtime/compute/vm; domains/ec2/instance/status_selection.go:9 also imports runtime/compute/vm. Each reads VM or GPU runtime state directly instead of through a domain-owned projection.
  • STS web-identity verification never consults cluster status: tokens verify while a cluster is DELETING until teardown sweeps the prefix, and a FAILED launch keeps its JWKS. AWS behaviour must be established before calling it a divergence. Removal condition: AWS behaviour verified, and the resolver or EKS lifecycle aligned with it.
  • The public /keys endpoint (root gateway, oidc_discovery.go) serves malformed stored JWKS bytes with HTTP 200. Removal condition: validate before serving, or record it as accepted.
  • The public discovery and /keys endpoints return 404 for KV read errors, including timeouts; only a bucket-open failure returns 500. Removal condition: distinguish dependency failure from not-found.
  • The root gateway package imports handlers/eks directly (gateway/oidc_discovery.go: AccountBucketName, GetClusterMeta). The layering rules don't see it because the root gateway has no domain classification, so it has no allowlist entry. Removal condition: the public OIDC discovery and JWKS endpoints consume an EKS-owned resolver injected at composition, the same pattern as STS, leaving the root gateway with no handlers/eks import.
  • handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent (HealthReport, ServerStatus, CertDir, NginxPIDPath, NginxCertDir), a domain reading an agent's implementation package rather than a declared wire contract. Removal condition: the health-report and cert/PID-path vocabulary moves behind a guest/controller contract the domain and the agent both consume.
  • domains/ec2/instance/state/imds_readers.go (dependency, IMDS readers) imports daemon for LocalState, ReadLocalState, LocalStatePath, NewJetStreamManager and InstanceStateBucket. The local-file reader and the instance-record loader's JetStreamManager open, which vpcd/imds_instance_state.go held directly, moved here unchanged and are now injected into vpcd through guestmetadata.LocalStateReader and guestmetadata.RecordLoader. The readers already live in domains/ec2/instance/state. Removal condition: the legacy dependencies they call are extracted from daemon — LocalStatePath, ReadLocalState and LocalState, the instance bucket constants, and JetStreamManager with its bucket initialisation (InitKVBucket) — leaving domains/ec2/instance/state with no daemon import.
  • Advisory: 27 production files across domains/{acm,dns,ec2/*,network/host,network/ovn,ochre} import bootstrap/config directly rather than through typed constructor inputs, which ADR-0001's config row expects. bootstrap/preflight/preflight.go:15 imports runtime/host/command and services.go:13 imports operator/host/systemd; both are expected bootstrap-to-runtime/operator edges for startup validation and are not domain-rule violations.
  • Operator bootstrap credential generation: operator/cli/admin.go mints the bootstrap admin and Northstar access keys and secrets directly with awscredentials and writes them into config, rather than asking an IAM bootstrap capability to issue them. awscredentials only formats the values. Removal condition: a real IAM bootstrap capability issues these credentials and operator/cli calls it.
  • domains/dns: imports runtime/compute/vm (domain-to-runtime), domains/network/reconcile for the reconcile lock and bootstrap/config for node configuration.
  • ACM: the IAM AES-GCM helper import (ACM relocation residual).
  • runtime/roles/awsgw: the quota reconcile loop belongs to domains/admission/quota, and the EKS token-verify responder belongs with STS or EKS identity.
  • runtime/roles/vpcd (placement): the role still mixes process/role supervision with network-controller responsibilities. lease_reaper.go (orphaned external-pool DHCP lease sweep), ovn_controller_watchdog.go (OVN controller liveness and restart), host_eip_loop.go (host-EIP reconcile loop) and lease_datapath_reconcile.go (lease datapath reconcile) are periodic network-control loops, not role wiring; vpcd.go itself also carries bridge-mode detection and verification, chassis-list parsing, external-CIDR resolution, DHCP manager startup and lease/IP-change handling, and gateway/host-EIP allocator and neighbour flush/prime construction alongside Service.Start. The future placement question is whether these belong in domains/network. Removal condition: an accepted decision on network-controller placement, and extraction of those loops and helpers behind the role.
  • ECR control-plane registration: awsapi.Deps.NATS is a raw *nats.Conn, not a narrow capability. Every NATS-backed Handler passes it to domains/ecr.NewNATSMetaStore, which calls natsmsg.NATSRequest, so narrowing it means changing the exported Handler type, every handler and the meta store; it must not be broadened further meanwhile.
  • ACM control-plane registration: domains/acm/awsapi.Deps.NATS (registration.go) is likewise a raw *nats.Conn carried by every Handler, for the same reason; it must not be broadened further meanwhile.
  • cmd/aws-model-coverage imports ingress/aws/dispatch, domains/ecr/awsapi, domains/acm/awsapi and gateway to read ECR's and ACM's declared inventories. It is a non-serving tool and the only named exception to the ADR-0002 composition invariant; it must not construct a registration.
  • Ingress pipeline (ADR-0002 phase 4): the request-audit record, the trace action enricher, the throttle key functions and the gateway request-context keys stay in gateway until the auth/IAM and request-context boundary is designed. SigV4 authentication writes the audit record and context keys from four places, so moving them now would export exactly the setters ADR-0002 says must not be exported solely to make a directory move possible.
  • NAT projection delivery is inconsistent and is preserved as found: EIP AssociateAddress sends vpc.add-nat unacknowledged (AnnounceNATAdd), skipping the 45s OVN barrier every other add path waits on, and DeleteNetworkInterface sends vpc.delete-nat unacknowledged (AnnounceNATDelete), skipping the ack and the ErrNoResponders retry. The reconciler's orphan sweep is the only backstop for both. Clears when those call sites use AddNAT/AddNATBestEffort and RemoveNAT, a reliability change of its own.
  • vpc.add-system-egress has a vpcd subscriber and a contract entry but no producer (its former domains/ec2/eip publisher is gone); handlers/eks still publishes the delete. Either a producer is missing or the add route is dead; it needs an owner decision.
  • EBS encryption posture: domains/ec2/ebs/encryption reads the Viperblock key-file setting to decide what EC2 reports. Clears when providers/ebs reports each volume's encryption posture and EC2 stops reading key files.
  • foundation/messaging/nats/nats_trace.go still names its OpenTelemetry tracer github.com/mulgadc/spinifex/spinifex/utils. It is an emitted instrumentation-scope name that dashboards may filter on, so renaming it is a telemetry change, not a path fix.
  • runtime/compute/cache.New: two construction-time panics carry a lint exemption pending a constructor that returns an error.
  • domains/ec2/systeminstance: managed-service parent metadata is only the ManagedBy tag value.
  • gateway/bedrock: holds the Ochre domain core (model catalogue, provisioned throughput, guardrail, access, credential, usage, price, logging and weights stores), the inference engine (provider, vLLM, Anthropic, embeddings, reranker, admission) and the Bedrock action layer in one package with internal cycles. domains/ochre imports it, which is the ADR step 7 Ochre gateway import. Splitting it needs a plan: domain core and stores to domains/ochre/<resource>, inference to an Ochre-owned package, and only the action layer to domains/ochre/awsapi. The top-level gateway/bedrock*.go HTTP handlers additionally wait for the ingress registration seam.
  • domains/network: reconcile/intent.go builds desired network state by reading EC2's KV buckets and decoding EC2's private VPC, subnet, ENI, route-table, gateway, EIP, NAT and security-group records, and reconcile/drift.go watches those buckets. ADR-0001 names this pair explicitly: EC2 and network communicate through a capability/projection or versioned contract, never by one being the other's KV reader. Step 7 fix: an EC2-owned domains/ec2/projection exposing network intent with list and watch (it may stay KV-backed, so no data moves); a narrow intent interface declared by reconcile; wiring at the composition root; and the duplicated PurposeEIP/PurposeIGWLRP constants and mirrored SGRule/SGEvent folded into the projection or contracts/ec2/v1. The drift-watch coverage test and reconcile scenario tests are the evidence the projection must keep. domains/network also imports runtime/compute/vm and runtime/host/command (domain-to-runtime).
  • domains/ec2/instance: imports runtime/compute/vm and runtime/compute/gpu (domain-to-runtime) and domains/dns (cross-domain implementation import).
  • domains/ec2/instance/state (dependency): the vpcd IMDS local reader imports runtime/compute/vm to return vm.VM. Removal condition: that type moves into a domain-owned package that runtime/compute/vm consumes (runtime to domain is allowed).
  • agents/eks/tokenwebhook: imports handlers/eks for WebhookTokenReviewResult, an agent depending on a domain implementation. Temporary: the eventual EKS contract or projection for the token-review reply needs its own design, and no contract is invented to complete the move.
  • agents/ecs/agent: takes its wire types from contracts/ecs/v1, and still imports handlers/ecs for the poll-assignment and GPU-report inputs and outputs and the instance-type and availability-zone attribute names. Temporary agent-to-domain debt; no contract is created during the relocation.
  • agents/rds/agent (wire contract): imports handlers/rds for the command, reply, parameter, health and bootstrap types exchanged with the control plane. Temporary agent-to-domain debt; no contract is created during the relocation. The earlier LookupEngine dependency on the RDS domain's engine registry has since cleared: controlPlaneRulesFrom and both engine constructors now take the agent-declared EngineCatalog (see the narrowed entry below), with zero production uses of LookupEngine remaining in agents/rds/agent.
  • operator/installer/firstboot (security): Config.InstallCallback is rendered into spinifex-firstboot.sh, which is world-readable (0755). The field is currently unused (fireInstallCallback reads SPINIFEX_INSTALL_CALLBACK from the environment instead), but any future wiring must not place secrets or tokenised callback URLs in the generated script; sensitive callback credentials should reach first boot through a protected channel such as a 0600 file or a credential store.
  • operator/cli (known security risk): spx admin join contacts the formation server with InsecureSkipVerify, sends the join token as a bearer credential and receives the cluster CA certificate and key, master key, Viperblock key and shared credentials, so an on-path attacker during formation can capture the token and the cluster root secrets or substitute a CA. This predates the move; lint surfaced it when the tree left cmd/. It is accepted for now and carries a line-level gosec annotation stating it is not a false positive. Removal condition: authenticate formation TLS with a pinned key or certificate delivered with the join material, or an out-of-band CA, with tests proving a mismatched server certificate fails before credentials are sent.
  • handlers/eks (potential security risk): ClusterMeta.ControlPlaneTemplate (handlers/eks/service_impl.go) clears the template's credentials but keeps OIDCPrivateKeyPEM and the k3s JoinToken, so both are persisted in the eks-account-{acct} cluster record, which the gateway also reads. Whether that record is encrypted at rest is unconfirmed; if it is not, a reader of the bucket can forge service-account tokens for the cluster's OIDC issuer and join nodes to it. This predates the refactor and is not fixed in it. Removal condition: the key and token are held by a dedicated secret store (a future KMS or Secrets Manager capability is the likely owner) and the cluster record keeps only a reference, with a test proving the persisted record contains neither secret.
  • operator/cli (dependency): the command tree still imports domain and service implementations directly (handlers/{iam,eks}, domains/admission/quota, gateway, gateway/bedrock, daemon, vpcd, accountteardown, the Viperblock adapter and runtime roles). The ADR's cmd/spinifex current-to-target map row requires command packages to reach domains through public capabilities; splitting supported use cases into operator/* packages is later work.
  • providers/ebs/viperblock → runtime/compute/nbd and runtime/host/process (dependency, moved unchanged from services/viperblockd): the adapter formats NBD endpoints and manages nbdkit processes through runtime packages. ADR-0001 does not list providers → runtime as forbidden and the layering test does not enforce it (same category as providers/ebs/conformance → runtime/compute/nbd), but a provider should not depend on node runtime. Clears when NBD endpoint naming has a provider-facing home (a candidate for contracts/ebs/v1) and process supervision is injected by the role. The adapter also imports providers/objectstore, a provider-to-provider edge that is allowed.
  • gateway/ec2.go (registration): the EC2 dispatch map stays a GatewayConfig method in gateway and imports every domains/ec2/awsapi/* package; gateway/ec2_idempotency.go also imports domains/ec2/awsapi/idem (KVBucket, TTL, TokenAndParams) for the dispatch-time idempotency wrapper. ADR-0001 makes the domain own its action registration; moving the map waits on the generic ingress registration seam and must not be done as a package move.
  • gateway/requestaudit.go: the outermost request-audit middleware records AWS fields and is updated from four places in the gateway's authentication flow through the gateway's own context key. Moving it would export about seven names used only by that flow, so it stays in gateway until the generic ingress registration seam defines how ingress middleware receives authentication results.
  • accountteardown (dependency): its EC2 reapers call domains/ec2/awsapi/* action adapters directly, as they did under gateway/ec2. The ADR's accountteardown current-to-target map row replaces this with domain-owned cleanup capabilities before the package moves.
  • domains/admission/quota (dependency, moved from handlers/quota unchanged): live.go imports gateway/elbv2 and handlers/rds for its live ELBv2 and RDS counters; records.go imports runtime/compute/vm for the instance-record vCPU sweep; live.go, vcpu.go and storage.go call domains/ec2/awsapi/{vpc,eip,volume} describe adapters to count VPCs, subnets, addresses and volumes, as they did under gateway/ec2. The ADR's handlers/quota current-to-target map row replaces these with reader capabilities once their final locations exist.
  • domains/ec2/awsapi/instance (dependency): imports handlers/iam (IAMService in DescribeInstances.go, RunInstances.go and IamInstanceProfileAssociation.go; InstanceProfile in the latter two), runtime/compute/cache (NodeState, NodeUnknown, NodeLive) and runtime/compute/vm (VM), both in status_synthesis.go. The IAM edge needs an IAM capability; the runtime edges are domain-to-runtime and need the instance record and node liveness exposed through an EC2-owned projection.
  • domains/ec2/awsapi/spotinstance (dependency): imports handlers/iam.IAMService and domains/admission/quota.Service in RequestSpotInstances.go.
  • Instance adapter consumers outside EC2: gateway/rds/enginecatalog.go, daemon/eks_cp_control.go, daemon/rds_deps.go, domains/admission/quota's vcpu.go and gateway/iam.go (CountInstanceProfileAssociations) call domains/ec2/awsapi/instance directly; each needs an EC2 capability rather than the AWS action adapter.

Enforcement: the owner decided the layering test is blocking now, with a ratchet. Its allowlist mirrors this debt list exactly, each edge carrying its owner and removal condition; any new unrecorded forbidden edge fails local preflight and CI, and an allowlisted edge that no longer exists must be removed from the list.

  • handlers/rds → domains/rds/subnetgroup (legacy edges): instanceReferences binds Dependants by scanning instance records, and tags.go writes the group record through its generic tag registry. The first clears when the RDS instance gains its own owner and answers Dependants from it. The second clears when tagging invokes a subnet-group-owned tag/untag capability rather than writing the record; moving the tagging adapter to domains/rds/awsapi alone does not clear it.
  • handlers/rds → domains/rds/parametergroup (legacy edges): parameterGroupReferences binds Dependants by scanning instance records; this clears when the RDS instance gains its own owner and answers Dependants from it. tags.go writes the meta record through its generic tag registry; this clears when tagging invokes a parameter-group-owned tag mutation capability, and moving the tagging adapter to domains/rds/awsapi alone does not clear it. The Modify handler still calls propagateParameterGroup after the owner's write; propagation is instance-side realization and stays legacy until it is designed as asynchronous realization (group generation, instance staleness, retry). parameterGroupValues returns the persisted Record and overrides to instance code; when the instance owner is extracted, replace them with a consumer-owned resolved-parameters projection.
  • handlers/ecs → domains/ecs/taskdefinition (legacy edges): tags.go reads and writes the revision record through its generic tag registry; this clears when ECS tagging invokes an owner-provided tag mutation capability, and moving tags.go to domains/ecs/awsapi alone does not clear it. taskDefinitionResolver returns the persisted Record to task and service launch code; when the task and service owners are extracted, replace the record crossing that boundary with a consumer-owned resolved task-definition (launch-spec) projection. Moving packages has not made the record private.
  • domains/ecs/taskdefinition (deliberate duplication, not debt): private getJSON, putJSON and keysWithPrefix copy the raw-KV helpers in handlers/ecs so the extraction keeps ECS's get/put/list error and recovery semantics. Revisit only if a later, tested state abstraction proves it preserves those semantics; do not migrate ECS to kvstore merely to remove them.
  • handlers/eks → domains/eks/access (legacy edges): ResolveTokenReview opens the EKS bucket in the gateway process and reads records through the exported access.Get; this clears when token review asks the owner across a contract rather than opening the bucket itself. DeleteClusterPrefix erases access-entry keys as part of its blanket cluster sweep; this clears when cluster delete invokes an owner-provided removal of the cluster's entries. The owner returns the persisted Record to the AWS adapter and token review; replace it with consumer-owned projections when those consumers are extracted.
  • handlers/eks → domains/eks/addon (legacy edges): DeleteClusterPrefix erases add-on records and manifests in its blanket cluster sweep; this clears when cluster delete invokes an add-on-owned teardown capability. stageGPUDeviceAddon is a temporary node-group-to-add-on orchestration edge: best-effort, errors logged and swallowed, never undone on node-group delete; it clears when the node-group owner is extracted and the dependency is designed. ClusterReconciler hosts the add-on status subscription; it clears when add-on delivery observation has its own host under the lifecycle correction. Cluster existence is checked in the handler, not through an owner-declared capability; it moves into the owner when cluster admission is defined (today status is ignored). Guest wire types (AddonStatusReport, AddonDeliveryPhase, AddonStatusSubject, StagedAddonManifest, InternalAddonsResponse) are owned by contracts/eks/v1, which handlers/eks, gateway/eks and agents/eks/gatewayfetch import directly.
  • handlers/eks and gateway/eks → domains/eks/addon (further edges): the AWS adapter reads the persisted Record through addon.Get/addon.List; this clears when the owner returns a view and the add-on adapter moves to domains/eks/awsapi. The guest route reaches the owner through the handlers_eks NATS RPC (ListStagedAddonManifests); this clears with per-resource commands behind EKS registration. The add-on ARN is derived in both the owner and gateway/eks/authz.go; this clears when authorization obtains the ARN from the owner, which an incarnation suffix would require. The owner's Manifest/Report beside the contracts/eks/v1 types is deliberate duplication, not debt.
  • agents/rds/agent (dependency, narrowed): engine metadata now arrives through the agent-declared EngineCatalog, but the agent still imports handlers/rds for the command protocol, wire and record types (Command*, Parameter, EngineHealth*, BootstrapMode*) and API output types. This clears when those move behind a guest/controller wire contract.
  • gateway/rds → handlers/rds (InstanceSizing): the gateway obtains its Sizing through the RDS handler package's adapter rather than its own composition root. This clears when the gateway's RDS composition moves to runtime/roles/awsgw and builds Sizing there.
  • domains/admission/quota → domains/ec2/instancetypes: reconcile.go, records.go and vcpu.go read DefaultVCPUs to size instances without a recorded vCPU count. This clears when quota receives vCPU sizing through a consumer-owned capability or an EC2 projection.
  • IAM implementation consumers in STS, Ochre and ELBv2: handlers/sts imports handlers/iam across five files (IAMService, SessionRevoker, EncryptSecret/DecryptSecret, AccessKeyStatusActive, ValidateTrustPolicyDocument, PolicyEffectAllow/PolicyEffectDeny, IAMAccountBucketName, OIDCProviderARN, OIDCProviderKey), not only oidc_jwks.go; gateway/sts/GetCallerIdentity.go takes IAMService; gateway/bedrock/credentials.go uses EncryptSecret/DecryptSecret; and handlers/elbv2/{lb_role,service_impl}.go use EnsureSystemInstanceProfile and SystemInstanceRoleEnsurer. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
  • handlers/elbv2 (cross-domain): service_impl.go imports domains/acm (Store, NewStore), domains/dns (ELBChanges, ELBName, Change, ActionUpsert, ResolveBaseDomain), domains/ec2/vpc (VPCServiceImpl) and domains/network/topology (Port), and launcher.go re-exports domains/ec2/systeminstance launch types as aliases. These clear when ELBv2 is extracted and each edge becomes a consumer-owned capability, which ADR-0001 sequences after the ELBv2 data-plane boundary is settled.
  • docs/service-interfaces.yaml (ECS coverage gap, closed): the traced ecs.bus.* family and the ECS AWS-API action subjects are both listed. The action subjects are declared as ecs.*, the same wildcard convention as rds.*: handlers/ecs/service_nats.go publishes them from the gateway and daemon/daemon.go subscribes to them. Declaring ecs.bus.* alone had made ecs a managed prefix, so manifest-lint failed CI on all 34 undeclared action subjects until this was added.
  • contracts/ecs/v1 (dead routes): four of five bus subjects have no producer. The scheduler leader still subscribes to instance-register, instance-heartbeat and task-state, which can never fire, and assign has neither producer nor consumer; only service-reconcile is live. Removal condition: an owner decision to retire these routes (a successor contract under the compatibility rules) or to adopt them in the agent.

The layering test enforcing these rules (architecture/layering, a module-root repository governance test, not Spinifex product code; run by make test, test-cover and test-race) is blocking, with a ratchet: the debt recorded in this section is narrowly allow-listed by owner and removal condition, and any new, unrecorded forbidden edge fails local preflight and CI. The allow-list mirrors the edges in this section that break an import rule; same-domain legacy edges, placement residuals and security notes are not import-rule violations and have no allow-list entry.

  • Pointer-bump follow-up in the umbrella repository: make docs-portal for this branch's source moves is deferred to the Mulga gitlink pointer-bump PR, so the portal evaluates the same Spinifex revision being proposed.
  • Merge-time follow-up in the umbrella repository: the Makefile invariants-check target, the docs-pr.yml clause cross-check, .claude/hooks/architecture-traps.sh and a check-invariants.sh comment hard-code spinifex/spinifex/network/invariants. They must change with the pointer bump that brings in this branch, or the docs-pr cross-check silently skips. Live specs and proposals citing spinifex/network/ will report unresolved citations until updated. A read-only mulga-repository count (excluding submodules, docs/development/archive/ and agent worktrees) confirms four items still open: spinifex/network/ appears in 49 mulga files; services/awsgw appears in 19 (.claude/rules/spinifex.md plus 18 docs; the umbrella .claude/CLAUDE.md does not contain it but cites the old network/policy/policy.go:28); and services/spinifexui (including its /frontend subpath) appears in 20 mulga files, including ansible/roles/build/tasks/main.yml, the .claude/settings.json formatting hook, the .claude/rules/frontend-standards.md glob, .claude/skills/run-spinifex/SKILL.md and ui-dev.sh, .vscode/settings.json and README.md. The same SKILL.md also names cmd/spinifex/cmd, the obs-agent Ansible role's otelsetup path mirrors the pre-move name, and docs/specs/guides/dependency-enforcement.md's "Known open edges" section carries the same pre-move paths; all four are mulga-repository updates, out of scope for a Spinifex-only commit.

Security corrections and branch preflight exception

These are security corrections found during discovery, not lifecycle work.

  • ee13be950: PublishInternal required only the IAM policy, built from the body account and URL cluster, and the control-plane role grants it on *. Any control-plane VM session, any tenant holding eks:*, or static system credentials could publish cluster state, bootstrap and add-on reports for another cluster or account. It now passes the internal-route membership gate, with one body decoder shared by the gate and the handler. Evidence: gateway/eks_authz_test.go TestEKSRequest_PublishInternal*.
  • a23e1ccf5: WebhookTokenReview had the same flaw and could reveal another cluster's Kubernetes group mappings. It now passes the same gate, and every unauthorised target returns the same AccessDenied without reaching STS. Evidence: TestEKSRequest_WebhookTokenReview*, gateway/eks/internal_authz_test.go TestInternalBodyAccounts_CoverEveryBodyScopedRoute.
  • 642cb4889: serviceAccountRoleArn on CreateAddon and UpdateAddon must be syntactically an IAM role ARN (foundation/aws/arn.ValidateRoleARN), returning InvalidParameterException; iam:PassRole remains the permission and existence check. Evidence: handlers/eks/addons_test.go TestCreateAddon_RejectsMalformedServiceAccountRoleArn, TestUpdateAddon_*ServiceAccountRoleArn*.
  • 097e19109: CreateAddon and UpdateAddon accepted serviceAccountRoleArn with no iam:PassRole check, the gap the 52de5e517 characterization recorded ("no PassRole check on serviceAccountRoleArn"). PassedRoleARNs now resolves the field for both actions, so the caller needs iam:PassRole on the role for eks.amazonaws.com, matching CreateCluster and CreateNodegroup. Evidence: spinifex/gateway/eks_passrole_test.go.
  • Remaining render gap: IAM's path grammar permits ", &, \ and |, which the guest's sed replacement and double-quoted YAML value in mulga-eks-addon-sync.sh do not escape. A role whose path contains them must also exist and pass iam:PassRole. Removal condition: the guest renders the role ARN with escaping for both sed and YAML (or a structured renderer), with a test rendering such a path verbatim.
  • Static-credential control-plane VMs (IAM wiring failed) are now denied every internal route and cannot bootstrap; that fallback is unsupported.

Preflight exception (branch only): from ee13be950, full make preflight on this branch is red solely because the vulnerability gate reports GO-2026-6599 to GO-2026-6617 against the pinned Go 1.27.0 toolchain. Every other gate passes. This branch is not production. No advisory is accepted or suppressed, and the accepted-advisory list, go.mod and the toolchain pins are unchanged; the toolchain is updated through Dependabot. Normal full-preflight enforcement resumes when the toolchain baseline is updated. Resolved by b42373efb: merging dev brought the Go 1.27.2 toolchain, the vulnerability gate passes with no advisory accepted or suppressed, full make preflight is green, and normal enforcement has resumed.

Merge from dev (b42373efb): dev's lint fix replacing strings.LastIndex with strings.CutLast in actionFromTarget touched spinifex/gateway/acm.go and spinifex/gateway/ecrapi.go, which this branch had deleted when it moved those registrations. The change was ported to the moved code in spinifex/domains/acm/awsapi/registration.go and spinifex/domains/ecr/awsapi/registration.go, and the deletions were kept. The same merge carried dev's ACM tag private-key fix into spinifex/domains/acm with import paths adjusted to this branch's layout.

Merge from dev (316a1b7c0): dev's IMDS PUT hop-limit enforcement was ported into spinifex/domains/ec2/guestmetadata with the package renamed. dev's fail-closed instance inventory for account teardown (DescribeAccountInstancesComplete) landed in spinifex/domains/ec2/awsapi/instance, and its callers and tests in spinifex/accountteardown, spinifex/gateway and spinifex/operator/cli were adjusted to this branch's import paths (natsmsg.NodeIDHeader, awserrors.GenerateErrorPayload, ec2instanceapi). The spinifexui frontend source and dist were taken from dev at spinifex/runtime/roles/spinifexui/frontend, keeping only this branch's path-comment update in src/types/rds.ts.

Merge from dev (62870d350): dev's identity-checked volume holder proof for account teardown landed in spinifex/accountteardown and its DeleteVolume guard in spinifex/domains/ec2/awsapi/volume, with call sites renamed to this branch's packages (natsmsg, ec2volumeapi, ec2instanceapi). The new gateway guard test and the teardown test fixtures were adjusted to this branch's import paths, and public DeleteVolume passes gw.NodeIDs as on dev.

Gates outside preflight and CI (follow-up, not merge-blocking): make test-package-check, which the Makefile keeps out of preflight, reports 87 test files added on this branch in their own package rather than <pkg>_test; they include the contract, agent, new-domain, ingress, layering and split-out ECR tests. Converting each to an external test package, or marking it //test:in-package with a reason, is follow-up work before that gate returns to preflight. make nilaway was killed by the host on both local attempts (15 GB, including GOGC=25 GOMAXPROCS=2) before it reported anything, so this branch has no nilaway evidence.

ADR-0004 source inventory and first-resource ranking

The per-file inventories for ECS, EKS and RDS are in docs/package-boundary/adr-0004-inventory-{ecs,eks,rds}.md. Each classifies every production file as resource owner, cross-resource orchestration, AWS protocol adapter, guest/controller wire contract, or temporary residue, and maps actions, durable records, identity and scope, lifecycle paths, NATS subjects and boundary-crossing imports. They were produced by reading the code at 6c50349bd and are not verified against a live system.

Observed gaps common to all three: no resource carries a desired generation, all resources of a service share one per-account KV bucket, and deletes remove records before dependent cleanup is confirmed.

First-resource ranking:

  1. RDS DB subnet group: about 340 lines with its own db-subnet-groups/{name} keys, synchronous AWS semantics, and no quota, expected-node, IAM, Bluebottle or agent dependency. It has a real deletion graph, because delete refuses while an instance references the group, so it exercises a consumer-owned capability in both directions: an instance-owned "instances referencing this group" query for delete, and a subnet-group-owned placement query for instance create.
  2. ECS task definition: own keys and good tests, but RegisterTaskDefinition allocates revisions non-atomically and the gateway adapter carries the iam:PassRole checks, so it sits closer to a blocked boundary.
  3. EKS access entry: own keys, but entries are read during token review, which places it next to EKS identity.

Blocked dependencies recorded, not designed around:

  • RDS instance: quota admission, the duplicated expected-node fan-out (gateway/rds/enginecatalog.go, daemon/rds_deps.go), IAM ensurer and Bluebottle master key.
  • RDS DescribeOrderableDBInstanceOptions: the EC2 instance-type-availability projection.
  • ECS task, service and capacity: iam:PassRole in the gateway adapter, IAM ecsInstanceRole, and EC2/ELBv2/EIP adapters called as internal APIs.
  • EKS nodegroup and cluster: IAM role and instance-profile creation, Bluebottle auth, the NATS CA, and EKS identity (token review, OIDC).
  • Guest agents: agents/ecs, agents/rds and agents/eks/tokenwebhook import the service implementation packages, which ADR-0004 S5 forbids; they need wire contracts.

Findings to resolve before extraction:

  • dev is merged in (9e77181f2), including ModifyDBSubnetGroup (#1152); the subnet-group inventory is superseded by its lifecycle contract, docs/package-boundary/rds-db-subnet-group-lifecycle.md.
  • The EKS control-plane template persists the OIDC signing key and k3s join token; see the handlers/eks potential security risk in the dependency-rule debt.

ADR-0007 instance-state inventory (slice 0)

This is the baseline for ADR-0007 (instance state authority and partition recovery), produced by reading the code at cf9e8ce0d and pinned by tests in 38fbd1499 and 6fa09d336. Nothing here conforms to ADR-0007 yet. Every behaviour below is current behaviour, recorded so later slices can show what they change; none of it is a target. vpcd stays blocked: it still reads the daemon's local file and opens the instance bucket through the daemon's manager (INV-13), and no slice has given it a narrower capability.

Shared KV is primary for accepted lifecycle intent and assignment. The local file is a durable execution journal for an already-assigned local guest and is not authority to create or reassign resources. The characterisation below shows several places where today's code treats it as more than that.

Persisted state

Item Location Shape
Local file daemon/local_state.go <DataDir>/state/instance-state.json (default DataDir /var/lib/spinifex), mode 0640, parent 0750, written to a temp file then renamed. Body is {"schema_version":1,"vms":{<id>: vm.VM}}, compact JSON, no trailing newline, the whole vm.VM per instance, no generation of any kind.
Live bucket daemon/jetstream.go:130 spinifex-instance-state, history 1, no TTL, RecreateIfMissing, version stamp 5 via kvutil on open through migrate.DefaultRegistry.RunKV. Created at one replica and raised to cluster size by kvutil.RaiseAllBucketReplicas (daemon/daemon.go:2536).
Terminated bucket daemon/jetstream.go:146 spinifex-terminated-instances, history 1, TTL 1 hour, version stamp 3, same open hook.
Record keys daemon/instance_records.go i.<id> holds a vm.InstanceRecord (metadata, spec, status) in both buckets; the live bucket also holds node presence and ownership keys and the legacy per-node and stopped prefixes. Records carry no per-record schema version.
KV migrations daemon/instance_records_migrate.go Live 1→2 stopped instances to per-resource keys, 2→3 running sets to per-resource keys, 3→4 i/<id> to i.<id>, 4→5 node presence and ownership out of the running-set blob; terminated 1→2 per-resource keys, 2→3 re-key. operator/upgrade touches no instance state; its VERSIONS.md points KV migrations at the owning package.
Record type runtime/compute/vm/record.go vm.InstanceRecord with resource.Metadata (Generation, ObservedGeneration, UID, Region, OwnerRefs, Finalizers, Tags, DeletionTimestamp); VM.Record() and VMFromRecord convert to and from vm.VM.

Consumer inventory

The owner files (daemon/{instance_membership,instance_records,instance_records_migrate,instance_running_set,jetstream,local_state}.go and runtime/compute/vm/record.go) define the state and are not listed as consumers. Line numbers are the first use in each file.

Item Location Package Process Access
INV-01 daemon/daemon.go:2686, :2889, :2912, :2922 daemon daemon Reads the local file at boot (LoadState) and rewrites it whole in persistState, the single path every state change reaches, before the KV write and regardless of its outcome.
INV-02 daemon/daemon.go:221, :2485–:2495 daemon daemon Constructs the manager and opens both buckets, which runs the version gate and migrations.
INV-03 daemon/daemon.go:2936–:2937, daemon/instance_record_repair.go:65 daemon daemon Writes the node marker and projects the in-memory running set to i.<id> records (whole-record write from vm.VM).
INV-04 daemon/daemon.go:2423 daemon daemon Lists terminated instances (decodes to vm.VM).
INV-05 daemon/vm_adapters.go:32–:91 daemon daemon Adapts the manager into vm.StateStore: LoadState, stopped and terminated read, write, update, claim, list and delete, all through vm.VM.
INV-06 runtime/compute/vm/{migrate,shutdown,orphan_qemu_reaper,teardown_reaper}.go vm daemon Uses the StateStore port for stopped and terminated writes, terminated listing, update and delete.
INV-07 domains/ec2/instance/service_impl.go:511–:2847 ec2/instance daemon Stopped and terminated read, update, claim, write and delete through the StateStore-shaped port.
INV-08 daemon/daemon_handlers_image.go:35, :89 daemon daemon Loads and updates a stopped instance for image creation.
INV-09 daemon/daemon_mgmt_ip.go:18 daemon daemon Cluster-state consumer outside the ratchet: names JetStreamManager only for management-IP allocation in the cluster-state bucket and touches no instance key.
INV-10 daemon/instance_recovery.go:85–:546 daemon daemon Lists and decodes records, claims, releases and abandons recovery, reloads a record. Typed CAS writes.
INV-11 daemon/dns_reconcile.go:38, :48, :179 daemon daemon Constructs keys from the prefix, names the bucket, lists records for DNS.
INV-12 daemon/eni_orphan_reaper.go:26–:27 daemon daemon Lists live and terminated records to find orphaned ENIs.
INV-13 domains/ec2/instance/state/imds_readers.go:20–:92 domains/ec2/instance/state vpcd Reads and caches the daemon's local file by path and mtime; constructs a JetStreamManager and calls InitKVBucket, which runs the version gate and the instance migrations; decodes records. Injected into vpcd through guestmetadata.LocalStateReader and guestmetadata.RecordLoader.
INV-14 domains/ec2/guestmetadata/instance_lookup.go:60, :120 guestmetadata vpcd (IMDS) Loads a record through a LoadInstanceRecord interface satisfied by the manager and converts it to vm.VM.
INV-15 domains/network/reconcile/intent.go:48, :444–:474 network/reconcile vpcd Duplicates the bucket name and prefix as literals, opens the bucket by name with no gate, gets i.<id> and decodes vm.InstanceRecord.
INV-16 runtime/roles/awsgw/awsgw.go:573–:633 awsgw gateway Opens the bucket by name with no migration hook for the instance cache and the quota reconcile loop.
INV-17 runtime/compute/cache/cache.go:97, :248 instancecache gateway Watches i. records into a vm.VM map through kvstore.Store[vm.InstanceRecord].
INV-18 domains/admission/quota/records.go:33 quota gateway Snapshots records to count vCPUs.
INV-19 operator/cli/cluster.go:170 operator/cli operator CLI Cluster-state consumer outside the ratchet: constructs a JetStreamManager only for the cluster-state shutdown marker and touches no instance key.

Test packages are excluded; daemon/instance_records_vpcd_test.go pins INV-15's duplicated literals to the daemon constants.

Characterised behaviour

Local file (daemon/instance_state_baseline_test.go):

  • TestCurrentBehaviour_LocalStateFileExactBytes pins the exact bytes.
  • TestCurrentBehaviour_LocalStateSchemaVersionFailures: a missing or older schema_version reads as unknown schema_version 0 (expected 1), a newer one as unknown schema_version 2 (expected 1), a wrong type or truncated body as parse local state; every case returns no state. Daemon.LoadState turns any of these into a boot failure (TestDaemonLoadState_CorruptFile).
  • TestCurrentBehaviour_LocalStateUnknownFieldsAreDroppedOnRewrite: a file at the same version with fields this binary does not know is accepted, and those fields are gone after the next rewrite.
  • Already covered and relied on: daemon/local_state_test.go (TestWriteLocalState_GroupReadable, _AtomicReplace, _NoTmpLeftBehind, _MkdirParent, _Failures, TestReadLocalState_Missing, _CorruptJSON, _UnknownSchema, _Unreadable) and daemon/disconnect_test.go (TestDaemonWriteState_LocalFile, TestDaemonLoadState_*).

KV (daemon/instance_state_baseline_test.go):

  • TestCurrentBehaviour_InstanceBucketConfiguration pins names, history, TTL and version stamps.
  • TestCurrentBehaviour_RecordKeyAndWireForm pins the i.<id> key and the exact record bytes; the running-set writer stamps the node and AZ it was given over whatever the instance carried.
  • TestCurrentBehaviour_BucketVersionGate: a daemon-configured open of a bucket stamped ahead fails with migrate.SchemaAheadError, a handle already open keeps writing, and a reader configured without the migration hook (the gateway's form) opens and decodes regardless.
  • Already covered and relied on: daemon/instance_records_test.go (round trip, absent is nil, disjoint prefixes, TestWriteInstanceRecord_ReplacesWholesale, TestUpdateInstanceRecord_*), daemon/instance_claim_test.go (claim, release and abandon CAS), daemon/instance_running_set_test.go, daemon/instance_records_transition_test.go (every migration step), daemon/jetstream_test.go (TestJetStreamManager_InitBuckets_WritesVersion, _InstanceRecordKVErrors), foundation/state/kvutil/kvutil_test.go and foundation/state/migrate/kv_test.go.

Recovery on return (daemon/instance_state_recovery_baseline_internal_test.go), with the local file reloaded and restoreInstances run:

Case Current outcome Test
Canonical key missing Local instance kept; the next state write recreates the record from the local file, owned by this node. TestCurrentBehaviour_RestoreMissingCanonicalKeyRepublishesFromLocal
Live key missing, terminated record present Terminated bucket not consulted; the instance is revived from the local file and republished as live and non-terminal. TestCurrentBehaviour_RestoreIgnoresTerminatedBucket
Record owned by another node Local copy dropped and removed from the rewritten file; the other node's record is left untouched. TestCurrentBehaviour_RestoreRecordOwnedElsewhereDropsLocal
Terminal record owned by this node The canonical record wins the merge and the instance is migrated to the terminated bucket, not restarted. TestCurrentBehaviour_RestoreTerminalCanonicalRecordRetiresLocal
KV unavailable Restore stops after the failed cluster read; local instances stay as the file left them, are not reset or relaunched, and the file is rewritten unchanged. TestCurrentBehaviour_RestoreWithKVUnavailableLeavesLocalUnlaunched

The first two cases are the ones ADR-0007 S5 rules out: a node republishes local state as authority without the canonical record.

Owner disposition of the KV-unavailable result:

  • Correct: while canonical KV is unavailable, the daemon does not launch, recreate, migrate or report a healthy new realization.
  • Permitted later: reattaching only to a demonstrably already-running local guest covered by a valid local journal assignment and fence.
  • Not acceptable as the final model: keeping an in-memory record as running when no live process has been confirmed; the later recovery slice must make that an explicit unconfirmed or degraded state.

The safety goal is that a partition does not kill an existing workload, while a daemon restart plus a stale file cannot resurrect one. TestCurrentBehaviour_RestoreWithKVUnavailableLeavesLocalUnlaunched stays as the current-behaviour pin until that slice changes it.

Generation preservation

Generation and ObservedGeneration are the only generation fields; KV entry revisions are used for CAS and never persisted in the record. Unknown fields are a field a newer binary adds.

Path Generation and ObservedGeneration Unknown fields Test
UpdateInstanceRecord Preserved Lost TestCurrentBehaviour_GenerationAcrossKVWriters
LoadInstanceRecord then WriteInstanceRecord Preserved Lost same
ClaimRecoverableInstance, AbandonRecovery Preserved Lost same
ClaimStoppedInstance Preserved Lost same
UpdateTerminatedInstanceRecord Preserved Lost same
WriteRunningSet Lost Lost same
UpdateStoppedInstance, WriteStoppedInstance Lost Lost same
UpdateTerminatedInstance, WriteTerminatedInstance Lost Lost same
KV → LoadState → running set → recovery merge Lost Lost TestCurrentBehaviour_LoadStateDropsGeneration
Record → VMFromRecord → Record() Lost, with UID, Region, OwnerRefs, Finalizers and Metadata.Tags; Name, AccountID and DeletionTimestamp survive Lost runtime/compute/vm/record_generation_test.go TestCurrentBehaviour_RecordThroughVMLosesGenerationAndBookkeeping
vm.VM → Record() Never written n/a TestCurrentBehaviour_RecordFromVMHasNoGenerationKeys
Typed record decode then encode Preserved Lost at every level TestCurrentBehaviour_TypedRecordRoundTripDropsUnknownFields
vm.VM → local file → vm.VM Not represented Lost TestCurrentBehaviour_LocalStateFileExactBytes, TestCurrentBehaviour_LocalStateUnknownFieldsAreDroppedOnRewrite

Regression ratchet

architecture/instancestate parses every non-test production file with go/parser and fails on any new use of the daemon's instance-state bucket, key and local-file identifiers, vm.InstanceRecord, InstanceSpec, InstanceStatus, VMFromRecord and VM.Record(), the JetStreamManager instance-state methods (instance and terminated record access, the running-set projection, LoadState, and InitKVBucket and InitTerminatedInstanceBucket, which open the instance buckets), or the bucket and file names as string literals. The JetStreamManager type and NewJetStreamManager are not matched, because the manager also serves cluster state; a cluster-state-only consumer such as INV-09 or INV-19 is not instance-state debt and is not flagged. TestADR0007_S6_NoNewRawInstanceStateConsumers holds 67 allow-listed file and symbol pairs, each citing an inventory item above; the list only shrinks, a stale entry fails, and a walk that finds too little fails rather than passing vacuously. TestADR0007_S6_RatchetDetectsFaults and TestADR0007_S6_RatchetSeesBareIdentifiersInOwnerPackages inject synthetic sources to prove the matcher detects each kind of use, ignores declarations, and does not flag the manager type, its constructor or a cluster-state method. Adding a daemon.LocalStatePath use to runtime/roles/awsgw and renaming one allowlist file were each confirmed to fail the gate. After narrowing, a runtime/roles/awsgw file constructing a manager and calling InitClusterStateBucket passed, the same file calling WriteRunningSet failed NoUnrecordedConsumer, and restoring the removed INV-19 entry failed Allowlist_NoStaleEntries.

Close-out status

The table records every legacy directory still standing, what remains in it and what blocks its move. "Deferred: " names a recorded blocked-work series above; "needs decision" is open.

Legacy path What remains What blocks it
handlers/iam Private identity state and credential issuance; consumed by the EC2 instance and spot-instance AWS adapters, handlers/sts, gateway/sts, gateway/bedrock, the ELBv2 system-instance role path, ECR/Ochre signing-key crypto and operator/cli. Deferred: the ADR's handlers/iam row states a shared IAM contract is not inferred from current KV readers; each cross-domain reader needs an IAM capability first.
handlers/sts Everything except the EKS OIDC JWKS lookup, which now reaches handlers/eks only through the injected ClusterJWKSResolver. Deferred: per-consumer IAM capabilities (dependency-rule debt above); handlers/sts still imports handlers/iam directly.
handlers/ecs Task, service and capacity resources (taskdefinition already extracted to domains/ecs/taskdefinition). Deferred: ADR-0004 blocked-dependency list — iam:PassRole in the gateway adapter and EC2/ELBv2/EIP adapters called as internal APIs.
handlers/eks Nodegroup and cluster resources (access and addon already extracted). Deferred: ADR-0004 blocked-dependency list — IAM role/instance-profile creation, Bluebottle auth, the NATS CA, and EKS identity (token review, OIDC).
handlers/rds The RDS instance resource (subnet group, parameter group and engine catalogue already extracted). Deferred: ADR-0004 blocked-dependency list — quota admission, the duplicated expected-node fan-out, the IAM ensurer and the Bluebottle master key.
handlers/elbv2 Everything; not yet touched. Deferred: ADR-0001 sequences this after the ELBv2 data-plane boundary is settled. Needs decision: what evidence establishes that boundary is settled.
handlers/iam/mock A test double for handlers/iam. Moves with handlers/iam; it has no independent target.
gateway (root) Generic HTTP route assembly, SigV4/registry authentication, the policy gate, request-audit middleware, request-context keys, and the legacy per-service dispatch switch for every service not yet registered. Deferred: the ADR-0002 phases above (auth/IAM and request-context boundary design) gate moving the audit, context-key and legacy-dispatch mechanics.
gateway/bedrock The Ochre domain core, its stores and the inference engine, together with the Bedrock action layer, in one package with internal cycles. Deferred: needs the split plan recorded above — domain core and stores to domains/ochre/<resource>, inference to an Ochre-owned package, and only the action layer to domains/ochre/awsapi.
gateway/{ecs,eks,rds,iam,sts,elbv2} Each service's AWS action adapters and dispatch, not yet registered through ADR-0002. Deferred: the ADR-0002 registration contract has only reached ECR and ACM so far; each service needs its own registration slice.
gateway/spx promote_image.go and spx.go dispatch (storage.go already consumes contracts/operator/v1). Needs decision: which package owns operator-plane action dispatch registration — an ingress/aws-style seam for the operator plane, or gateway stays its home.
gateway/tagging The cross-service tagging API (GetResources) and its authorization. Decided: a new domains/tagging boundary owns the cross-resource tagging API and aggregation; individual resource domains keep ownership of their own tag storage and expose narrow tagging capabilities. Stays debt until those per-domain capabilities exist; clears when domains/tagging aggregates only through them.
services/viperblockd — Done. The adapter is providers/ebs/viperblock (with the read-only dirty reader), the shared vocabulary is contracts/viperblockd/legacy/v1, and the role is runtime/roles/viperblockd. services/ is empty.
utils — Done and deleted. ec2_subject.go went to contracts/ec2/v1, the vpcd protocol to contracts/network/v1 with the typed client in domains/network/projection, and the master-key loader to domains/ec2/ebs/encryption.
vpcd — Done. The IMDS local-file reader and record-space opener moved to domains/ec2/instance/state and are injected through guestmetadata.LocalStateReader/RecordLoader (recorded above), so vpcd no longer imports daemon directly; its inter-process boundary now exists (contracts/network/v1, with domains/network/subscribers as the consumer and domains/network/projection as the producer client). The package moved unchanged to runtime/roles/vpcd (recorded below), and the legacy directory is removed.
daemon Reserved as runtime/daemon's home only; not split or moved. Deferred: Q-28's write-side consolidation is a prerequisite ADR-0001 names explicitly.
admin — Done and deleted. The operator use cases moved to operator/admin, image promotion to domains/ec2/image, the bootstrap account identifiers to foundation/aws/identifiers, and the credential-format generators to foundation/aws/credentials (recorded above). Operator bootstrap's direct credential generation stays recorded as debt.
accountteardown The whole package, a frozen legacy root. Its direct cleanup calls into domains/ec2/awsapi/* action adapters, other domain and handler services, the object-store provider, raw NATS subjects and a raw KV bucket stay as recorded debt. Assessed for relocation and not moved. ADR-0001's accountteardown row requires conversion to domain-owned cleanup capabilities before moving, and ADR-0008 does not authorise a mechanical rename. It remains a legacy root until its direct cleanup calls are converted to domain-owned capabilities; its destination after that conversion is operator/accountlifecycle.
lbagent The whole package; handlers/elbv2 imports it as a domain-to-agent implementation edge (recorded above). Deferred: Q-30 reserves agents/elbv2 as its destination but forbids moving it as an isolated rename ahead of the common-agent design.
migrate Nothing at the old path. Split, not an exact ADR row: ADR-0001 maps migrate to foundation/state/migrate, but the package held two responsibilities. The KV migration engine and its tests are in foundation/state/migrate (8cd9cff75); the config-file and object-store upgrade workflow moved to operator/upgrade (919d5bfa6) by owner decision. Needs an ADR-0001 map clarification before this row can be claimed as target conformance.

Remaining legacy roots by blocker class

Five legacy roots remain: accountteardown, daemon, gateway, handlers and lbagent. The rows above give each path's blocker; this summary groups them so a deferral is not mistaken for unfinished mechanical work. ADR-0001 completion is structural, so none of these is cleared by remediating service lifecycle behaviour.

Class Paths Status
Explicitly deferred by ADR-0001 daemon (Q-28), lbagent (Q-30) Closed for this refactor; the ADR names the prerequisite.
No missing boundary (candidate moves) handlers/ecs/bus → contracts/ecs/v1 (a130f6b8e); admin/images_promote.go → domains/ec2/image (263c58167); admin bootstrap account identifiers → foundation/aws/identifiers (3fdaa54d9); admin credential-format generators → foundation/aws/credentials 0368e861a, retiring admin Done.
One narrow missing boundary Done: handlers/sts EKS JWKS lookup → handlers/eks ClusterJWKSResolver (254d898a3). Open: gateway/tagging (per-domain tagging capabilities, owner decided); gateway/spx (owner of operator-plane dispatch registration) Each needs one decision or one capability, then a mechanical move.
Broader boundary or follows another package handlers/iam and handlers/iam/mock (per-consumer IAM capabilities); handlers/{ecs,eks,rds} (ADR-0004 blocked-dependency lists); handlers/elbv2 (data-plane boundary evidence); accountteardown (domain cleanup capabilities); gateway root (ADR-0002 auth and request-context phase); gateway/{iam,sts,ecs,eks,rds,elbv2} (each follows its handlers/<svc>, because a new-path awsapi cannot import a legacy handler); gateway/bedrock (the recorded Ochre split plan) Recorded debt with removal conditions above.

Per-consumer IAM capabilities are the widest unblocker: they gate handlers/iam itself, STS's IAM reads, Ochre's IAM reads, the ELBv2 role path and the ECS, EKS and RDS ensurers.

runtime/roles/awsgw carries two recorded residuals from its own move (931d86d5c above) that are not yet re-homed: runQuotaReconcile/quotaKeyFor belong to domains/admission/quota, which would become a domain-to-daemon edge if the reconcile loop stays where it runs today; and the EKS token-verify responder belongs with STS or EKS identity.

Deliberately kept on their old names for telemetry continuity, not debt: providers/ebs/telemetry.go keeps package ebsprovider, its tracerName (.../spinifex/ebsprovider) and ebs.-prefixed span/attribute names; runtime/compute/cache/metrics.go keeps package instancecache and the mulga.instancecache.* counter names; runtime/compute/vm/lifecycle.go keeps vmTracerName as .../spinifex/vm; and runtime/roles/qmpcollector keeps its qmp-collector: log prefix. Changing any of these is a telemetry and APM-query change, not a package move.

Image inputs deliberately left on their pre-move paths, for the same reason a structural move must not alter delivery material: images/mkosi.profiles/ecs/mkosi.conf and scripts/images/ecs-agent/manifest.conf still comment-reference cmd/ecs-agent/credendpoint.go (now agents/ecs/agent/credendpoint.go); images/mkosi.profiles/vllm/bedrock-serve_test.sh and its sibling bedrock-serve script keep their Bedrock-era names; and build/systemd/spinifex-viperblock.service keeps a comment naming utils.KillProcess, as already recorded above.

Recording rule

For every later slice, add the old and new path, source commit, focused verification, and any finding exposed by the move. Record a conflict rather than folding a behavioural repair into a structural commit. When this branch is merged, the umbrella repository's execution plan records the resulting Spinifex commit.