Skip to content

v0.0.1-alpha.37

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 24 Aug 01:29
f206efc

Overcast v0.0.1-alpha.37

Docker Images

Full image with the web management console:

  • Pull: ghcr.io/neaox/overcast:0.0.1-alpha.37
  • Moving tag: ghcr.io/neaox/overcast:alpha
  • Registry: overcast package

Headless slim image for CI pipelines:

  • Pull: ghcr.io/neaox/overcast-slim:0.0.1-alpha.37
  • Moving tag: ghcr.io/neaox/overcast-slim:alpha
  • Registry: overcast-slim package
docker run --rm -p 4566:4566 -p 4567:4567 ghcr.io/neaox/overcast:0.0.1-alpha.37
docker run --rm -p 4566:4566 ghcr.io/neaox/overcast-slim:0.0.1-alpha.37

Native Binaries

Download a binary for your platform from the assets below and verify it with SHA256SUMS.

Asset SHA256
overcast-darwin-amd64 6a9bb5e5293b2e0a1c9576a2eea9dbff9d1063e8c42c8ac20066c3d18f6185eb
overcast-darwin-arm64 e1577756fb25fe0ef6a43d4da0ba11fef4a132a91c0823ce6f7400b069927a5c
overcast-linux-amd64 91108ec5f743e46ad11d330cfeb49331ee40b3eb51ceef22f23ada342bfdb584
overcast-linux-arm64 ab4e2e8cac426bbdeb8c4c879de0aa9eaf7e82a9694d39d0a84a2ccec2367447
overcast-windows-amd64.exe ba2998f0ebaaf21fdb8a41fdd6a25541d3f77bf2fa9979d8d9f2e2f4a1892ea3
overcastd-darwin-amd64 4822ffaf5deacf352e1e72236f4384d680802dffe507eb1b1d5868bbb732a24d
overcastd-darwin-arm64 ae8a07a486ec0a16644ec57269d4f2c47344eba42039cadc88920bf5f41ef103
overcastd-linux-amd64 493429a1968d5a3492d66b4a26eecd7f5acf929a530864381467f5a29fed75ac
overcastd-linux-arm64 62989659ded2dac954244aa4f6db578381f103b14f43f41b09b2af965e146a77
overcastd-windows-amd64.exe 95e546c5a02f26f47779377b53eade1e2bf00263822e1be12c6ec2bc32ad0b43

Release Notes

Added

  • [athena] CreateWorkGroup persists Configuration and GetWorkGroup returns it; it was previously discarded

  • [backup/cloudwatch-logs] tag operations for the resources that had none. Backup gains TagResource/ListTags/UntagResource (POST /tags/{ResourceArn}, GET /tags/{ResourceArn}, POST /untag/{ResourceArn}), with tags stored inline on the vault or plan record so they die with it, and the CloudFormation provisioner forwards BackupVaultTags/BackupPlanTags and reconciles a tag-only stack update in place. CloudWatch Logs gains the modern TagResource/UntagResource/ListTagsForResource trio for log groups, resolving a resource ARN to the log group name and sharing the existing TagLogGroup/UntagLogGroup/ListTagsLogGroup spelling's validation and storage rather than duplicating it

  • [ci] a Lockfile freshness check fails a pull request whose web/pnpm-lock.yaml was generated against a main that has since changed its own copy, and a sweep on every lockfile push to main re-judges the open pull requests so a stale one is blocked before it merges rather than after — the case that kept every PR red for 28 minutes on 2026-08-22. It is not a merge queue: two lockfile PRs auto-merging in the same instant can still race

  • [ci] a pruned Smithy shape snapshot under models/aws/shapes/, regenerated and digest-verified alongside the AWS operation manifest

  • [cloudformation] ContinueUpdateRollback is now supported, so a stack in UPDATE_ROLLBACK_FAILED is recoverable rather than a stack you have to delete and redeploy. That state is reached when an update fails and the automatic rollback fails too — usually because both were blocked by the same thing outside the stack, a host port already bound or a resource something else is still holding — and it has no last known stable state, so every later UpdateStack, ExecuteChangeSet and change set has nowhere to start from; previously the operation returned 501, and the only way forward was delete-stack and a deploy from scratch. Clearing the blocker and calling it now resumes the rollback: the deletes that failed are retried, what the failed attempt half-created is retired, and the stack reaches UPDATE_ROLLBACK_COMPLETE and is updatable again. A retry that meets the same blocker lands back in UPDATE_ROLLBACK_FAILED and can be tried again. Nested stacks are continued before their parent, so a wedged child is recovered rather than reported rolled back from above. ResourcesToSkip is honoured for a resource that cannot be cleaned up at all, AWS's NestedStackName.ResourceLogicalID form included: the resource is left physically untouched, reported UPDATE_COMPLETE, and the rest of the stack rolls back around it. Any other stack status gets AWS's Stack:<arn> is in <status> state and can not be updated., and every ResourcesToSkip member is validated before the operation is accepted

  • [cloudformation] AWS::CloudTrail::Trail, AWS::Transfer::Server, AWS::Transfer::User, AWS::IAM::ManagedPolicy and AWS::IAM::InstanceProfile now merge a stack's own tags onto the resources they create, and reconcile a stack-tag-only change on update, matching every other propagating resource type

  • [cloudformation/web] a deployed stack now says which resources are stubs or backed by an inert/stub-tier service, on the same ResourceStatusReason CloudFormation already exposes and in the console's stack view, instead of only a server log line nobody sees

  • [cloudwatch] CloudWatch answers Smithy RPC v2 CBOR. The pinned model declares rpcv2Cbor for CloudWatch — the only dispatched service it declares it for — and a newer AWS SDK major negotiates it for a service that declares it, but Overcast claimed the protocol nowhere: its Smithy RPC registration carried no protocol service, so all fifteen dispatched operations answered 501 Not Implemented over POST /service/GraniteServiceVersion20100801/operation/<Op> while the identical call over the JSON or Query protocol worked. All fifteen now answer over CBOR, from the same implementation the other two protocols use — the request and response shapes and every validation rule are shared, so a call returns the same result whichever protocol an SDK picks, and PutMetricAlarm still marks the alarms Overcast will not evaluate. The router's protocol-symmetry gate consequently exercises CloudWatch over CBOR for real, which it could not before: an operation added to the Query and JSON tables without a CBOR binding now fails the build instead of quietly becoming a fourth protocol gap

  • [cognito] LambdaConfig triggers (PreSignUp, PostConfirmation, PreTokenGeneration, PostAuthentication, CustomMessage) are now invoked during SignUp, ConfirmSignUp, AdminCreateUser, AdminConfirmSignUp, ResendConfirmationCode, ForgotPassword, ConfirmForgotPassword, InitiateAuth, AdminInitiateAuth and RespondToAuthChallenge on the classic X-Amz-Target API, through the same synchronous Lambda invoker Secrets Manager rotation, API Gateway and AppSync already use. A trigger that errors or is unreachable fails the call with UserLambdaValidationException instead of being silently skipped, except PostConfirmation, which AWS documents as fire-and-forget since the account is already confirmed by the time it runs. CUSTOM_AUTH's DefineAuthChallenge/CreateAuthChallenge/VerifyAuthChallengeResponse remain tracked separately (#88, #94, #101); PreAuthentication, UserMigration and the Smithy RPC v2 duplicate dispatch path's non-token triggers are not yet wired

  • [compat] four new registry groups. rds-clusters covers the Aurora DB cluster lifecycle — create, describe, modify, stop, start, delete — and rds-cluster-members covers a cluster with an instance in it: DBClusterMembers, writer election, the settings a member inherits from its cluster, and the membership pruning DeleteDBInstance does. Both run in the Go, Python, Node and Java SDK suites and the CLI suite, over ground nothing outside the emulator's own Go tests had ever looked at, and they are what found the cluster response-envelope and membership fixes below. eventbridge-patterns (TestEventPattern, TestEventPatternNoMatch) and lambda-invoke-error (a handler that throws must come back as HTTP 200 with FunctionError set) are implemented in all seven SDK/CLI suites; eventbridge-patterns is the dotnet-sdk and rust-sdk suites' first EventBridge coverage

  • [config] the startup log names the resolved bind address(es), says that OVERCAST_LISTEN is what changes them, and says why the default was chosen — containerised or native

  • [config] Overcast warns once at startup when OVERCAST_HOSTNAME's subdomains do not resolve on this host — a resolver limitation (Windows does not resolve *.localhost the way Linux does) that otherwise surfaces far from its cause: virtual-hosted-style S3 addressing breaks silently, and cdk deploy asset publishing fails with a bare getaddrinfo ENOTFOUND on a bucket-shaped hostname. The probe is two DNS lookups run off the startup critical path — the bare hostname, then a synthetic subdomain of it — and only warns, never fails, since path-style S3 addressing is unaffected; the message also flags the case where a custom hostname is resolved only through a hosts file, where the warning is an expected false positive rather than a bug

  • [config] LocalStack's environment variables are accepted as compatibility aliases for their Overcast equivalents. LOCALSTACK_HOST maps to OVERCAST_HOSTNAME — LocalStack's documented hostname[:port] format is parsed and a port part must match OVERCAST_PORT — and EDGE_PORT, GATEWAY_LISTEN, DEFAULT_REGION, DATA_DIR, DEBUG, PERSISTENCE, HOSTNAME_EXTERNAL and LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT map to the matching OVERCAST_*/LAMBDA_* setting, completing the drop-in-replacement audit. SERVICES, LOCALSTACK_API_KEY and LOCALSTACK_AUTH_TOKEN are recognised and logged once at startup as inert rather than silently ignored. Every alias shares one mechanism and one conflict rule: a value that disagrees with an explicit OVERCAST_* setting fails startup naming both, rather than silently preferring one. docs/migration-from-localstack.md records which further variables were deliberately not aliased and why — LAMBDA_DOCKER_NETWORK, LAMBDA_KEEPALIVE_MS, the CORS variables, EAGER_SERVICE_LOADING, SNAPSHOT_* — rather than half-mapping them

  • [devex] Overcast warns, once per session, about four more environment mistakes that read exactly like emulator bugs: Docker unreachable for one or more container-backed services (naming which, and why); a request addressed to a real AWS hostname answered by Overcast instead (a stray hosts-file or DNS redirect); the API's published port differing from its listen port in a way URL rewriting cannot fix (a value compared rather than dialled); and memory-only storage about to silently ignore a database that already has data in it. Each fires as one actionable WARN at the moment the symptom appears, never speculatively on a healthy setup — see the new Troubleshooting section in docs/README.md

  • [ecs] a service's deploymentConfiguration now decides how many tasks its rollout runs at once. maximumPercent and minimumHealthyPercent were stored and echoed back by DescribeServices but never acted on, so every rolling update was a start-then-stop whatever the service asked for — and that is the ordering a shared Docker host cannot always take. A task whose port mapping carries a hostPort publishes it on the one host, so while both deployments are alive they contend for it and the replacement cannot start; on AWS each awsvpc task has its own ENI and two tasks never collide. The settings now resolve against desiredCount as AWS resolves them — maximumPercent a ceiling on tasks that have not stopped, across both deployments, rounded down; minimumHealthyPercent a floor on tasks reporting RUNNING, rounded up — and the superseded tasks are retired before the replacements are placed when the floor allows it. maximumPercent: 100 with minimumHealthyPercent: 0 (CDK's maxHealthyPercent/minHealthyPercent) therefore deploys such a service cleanly, at the cost of a moment of downtime, which is the same trade AWS offers. The defaults are unchanged at AWS's 200/100, so a service that sets neither deploys exactly as before. 100/100 cannot make progress at any desired count — nothing may be retired and nothing more placed — and AWS stalls there silently; Overcast stalls too but says once why, with a routine service event CloudFormation will not mistake for a stack failure

  • [eventbridge] TestEventPattern is implemented. It evaluates an event against a pattern with the same matcher PutEvents uses to select rules, so a pattern that passes here is one a rule would fire on; an unparseable pattern is the documented InvalidEventPatternException rather than a silent Result: false. It used to return 501

  • [eventbridge/ec2/ecs/stepfunctions] state changes now publish AWS-shaped events onto the default bus, so a rule matching one actually fires. EC2 Instance State-change Notification comes from RunInstances, StartInstances, StopInstances, TerminateInstances and the scheduler callbacks that fast-forward pending/stopping; ECS Task State Change from RunTask, StopTask and container-exit-driven stops; and Step Functions Execution Status Change from a standard workflow's initial RUNNING record, its SUCCEEDED/FAILED/TIMED_OUT/ABORTED terminal write, and StopExecution's direct ABORTED write — EXPRESS workflows are excluded, matching AWS

  • [inert] internal/inert/conformance turns the inert contract into executable, table-driven conformance tests for future Tier 1 services, with a naive-stub meta-test that pins the exact set of clauses a sloppy hand-rolled emulator violates

  • [lambda] extensions now start for container-image functions too. External extensions in /opt/extensions used to be launched by a shell shim that was only injected into zip-packaged containers; the in-container init launches them for every function, whatever it is packaged as, and tags each one's output with the extension's own name — so a Logs API subscriber receives them as extension records without the [overcast-extension:<name>] prefix Overcast used to write into the line

  • [lambda] the platform.initStart, platform.initRuntimeDone and platform.initReport Telemetry API records, with real timings. The in-container init is PID 1 in the execution environment and the proxy in front of the Runtime API, so it observes both ends of the INIT phase — the runtime being spawned, and the runtime's first GET /next — and reports them on the same sequence-ordered stream as the container's output. Under LogFormat: JSON they reach CloudWatch in the position AWS puts them in: initStart → the INIT phase's own output → initRuntimeDoneinitReportSTART, filtered by SystemLogLevel at AWS's documented levels (all three are DEBUG for a successful on-demand cold start, INFO for the report of a provisioned environment, WARN when the phase failed). Extensions subscribed to the Telemetry/Logs API receive them whatever the log format is, including the ones published before the extension's own process existed. Text-format log streams are unchanged, as they are on AWS.

  • [metrics/lambda/sqs/sns/dynamodb/apigateway/cloudwatch] emulated services now publish the AWS-native CloudWatch metrics real AWS publishes, so an alarm configured against one evaluates and fires end to end instead of silently never triggering. Lambda records Invocations, Errors, Duration, Throttles and ConcurrentExecutions once per invocation at the shared outcome boundary every path — sync, async, function URLs, event source mappings — already funnels through, plus the asynchronous set: AsyncEventsReceived for every accepted Event invocation, AsyncEventAge per retry attempt, AsyncEventsDropped when an event outlives its MaximumEventAgeInSeconds, and DeadLetterErrors/DestinationDeliveryFailures when a failed invocation's dead-letter or destination delivery itself fails. SQS records NumberOfMessagesSent/NumberOfMessagesReceived/NumberOfMessagesDeleted/NumberOfEmptyReceives/SentMessageSize per operation, plus a once-a-minute gauge sampler for ApproximateNumberOfMessagesVisible/NotVisible/Delayed and ApproximateAgeOfOldestMessage published for every active queue even with no traffic, as AWS does. SNS records NumberOfMessagesPublished/PublishSize on Publish/PublishBatch and NumberOfNotificationsDelivered/NumberOfNotificationsFailed per subscription delivery attempt. DynamoDB records SuccessfulRequestLatency, ConsumedReadCapacityUnits/ConsumedWriteCapacityUnits (with GlobalSecondaryIndexName where applicable) and UserErrors/SystemErrors across PutItem/GetItem/DeleteItem/UpdateItem/BatchGetItem/BatchWriteItem/Query/Scan/TransactWriteItems/TransactGetItems. API Gateway records Count/4XXError/5XXError/Latency/IntegrationLatency once per dispatched request, for both REST (v1, ApiName+Stage+Method+Resource) and HTTP (v2, ApiId+Stage+HttpMethod+RouteKey) APIs. All of it routes through one read-through and alarm evaluator, so GetMetricStatistics, GetMetricData and ListMetrics return the automatic series alongside custom PutMetricData data. The store is a sharded in-memory hot path with dedicated SQLite tables where available, keeping a 1-minute tier for 24 hours alongside 300-second (7-day) and 3600-second (30-day) rollups, so a 6h/24h/7d/30d chart has real data rather than only the last hour — and the whole substrate now survives a restart on OVERCAST_STATE=wal and every -tags nosqlite build, the one gap every earlier phase disclosed (OVERCAST_STATE=memory is unaffected: there is nothing for it to restart into). OVERCAST_SERVICE_METRICS=auto|enabled|disabled controls collection, default auto

  • [organizations] policies are stored for real — CreatePolicy, DescribePolicy, UpdatePolicy, DeletePolicy and ListPolicies, with TagResource, UntagResource and ListTagsForResource over the same tags. A policy's ID and ARN are derived from its name and stable across restarts, ListPolicies filters by the required type and rejects an invalid NextToken rather than restarting at page one, and the modeled errors are returned by name. Nothing a policy describes takes effect: AttachPolicy still returns 501, so no policy is ever attached or enforced. Tagging accepts policy IDs only — a root, OU or account ID returns TargetNotFoundException, since none of those are stored yet

  • [route53] Overcast's own DNS resolver now answers real queries from a hosted zone's records instead of storing them inertly. A/AAAA/CNAME (chased across zones, up to 8 hops)/MX/TXT/NS/SOA are served, including the apex NS/SOA every zone gets on creation; one-label wildcards (*.example.com) match exactly as AWS scopes them; TTLs come from the record's own stored value. ALIAS records resolve to Overcast's own reachable address, since that is the only correct answer for an alias pointed at an emulated ELB/CloudFront/S3-website/API Gateway hostname in this environment. NODATA and NXDOMAIN are distinguished correctly, both carrying the zone's SOA for negative caching. A hosted zone's records always take precedence over Overcast's own split-horizon emulator hostnames when both would claim a name. Private zones answer for every container (no VPC-association filtering, a documented deliberate simplification); public zones are still never advertised to the internet. See docs/services/route53.md for the full behaviour and documented divergences

  • [web] list and tabbed pages that already had a filter box now keep it in the URL (?q=) — AppSync, Cognito, KMS, SSM, Step Functions, and IAM/EventBridge, which also gained a deep-linkable ?tab= — so a filtered or tabbed view is shareable and survives a reload, and Back/Forward works; typing debounces into one history-replacing navigation instead of one per keystroke, following the same contract Request Traces' filters already used. ResourceTable/QueryListState also gained isFiltered/onClearFilter, so the empty state distinguishes "nothing exists yet" (create action) from "nothing matches the filter" (clear-filter action) instead of each page hand-writing that ternary, and a load error still always wins over both

  • [web] a copy control on the identifiers a detail page shows — ARNs, ids, domain names, lock tokens — named after the field it copies

  • [web/lambda/sqs/sns/dynamodb] Lambda function, SQS queue, SNS topic and DynamoDB table pages have a Monitor view charting the metrics the emulator now records automatically, over 1h/6h/24h/7d/30d ranges: Lambda's Invocations, Errors, Duration (average and maximum), Throttles and Concurrent Executions; SQS's messages sent, received, deleted and empty-receives plus the queue-depth and oldest-message-age gauges; SNS's messages published, publish size and notification deliveries/failures; and DynamoDB's consumed read and write capacity. Each reads through a narrow per-service allowlist endpoint — GET /_overcast/lambda/functions/{name}/metrics, /_overcast/sqs/queues/{name}/metrics, /_overcast/sns/topics/{topicName}/metrics, /_overcast/dynamodb/tables/{name}/metrics, never the CloudWatch protocol — and the matching BFF proxy under /api/

Changed

  • BREAKING [lambda] LAMBDA_PROACTIVE_INIT now defaults to true: after a settled function has trigger evidence (an invocation this process, a function URL, or an event source mapping), one execution environment is created in the background ~10 s after its last identity-changing write, so the next request lands warm instead of paying a cold start. This is the flip deferred from the cold-start plan after a release of opt-in soak surfaced no regressions. Proactive creation reserves capacity through the same admission budgets (instance count, memory including the high-water margin) a real invocation would, non-blockingly, so it can never queue ahead of real traffic; it never runs for functions with provisioned concurrency, an existing environment, or reserved concurrency of zero
    migration: set LAMBDA_PROACTIVE_INIT=false to opt back out and restore the previous on-demand-only behaviour

  • BREAKING [networking/ec2] the control plane is now created --internal whenever Overcast can tell it is safe — Overcast is itself containerised, or is running on a native Linux Docker daemon — closing the one documented egress path a gateway-less VPC's containers had: every VPC-placed resource without an internet gateway now genuinely has no route out, matching a private subnet with no NAT gateway. A native Overcast on a Docker Desktop host is unaffected — the probe declines there and the control plane stays an ordinary bridge; a containerised Overcast qualifies wherever it runs, Docker Desktop included
    migration: a Lambda function or ECS task in a VPC with no internet gateway that reached the real internet through this leak (a package fetch, an external HTTP call at cold start) now times out the way it would on AWS — attach an internet gateway, or set PubliclyAccessible (RDS) / assignPublicIp: ENABLED (ECS) per docs/networking.md, to restore reach

  • BREAKING [rds] the Aurora writer endpoint is now {cluster}.cluster.{region}.rds.{base}, not {cluster}.cluster-rw.…. cluster-rw was a label AWS has never used; dropping the account-specific hash from AWS's own {cluster}.cluster-{hash}.…, as every Overcast endpoint name does, leaves a bare cluster. The reader endpoint is unchanged
    migration: nothing to do if you read the endpoint from DescribeDBClusters or CloudFormation — the new name arrives on the next deploy. If you hard-coded or reconstructed …cluster-rw.… anywhere (task definition, secret, .env), replace it with the value the API returns; the old spelling keeps resolving for an existing cluster and stops once that cluster is recreated

  • [cloudformation] AWS::IAM::Role and AWS::IAM::User now also merge the stack's own tags, instead of only their own resource-level Tags

  • [dev] scripts/docker-go.sh, scripts/docker-go.ps1 and scripts/go.sh's Docker fallback take their default image from .devcontainer/Dockerfile's FROM line (golang:1.26-bookworm today) instead of a hardcoded golang:1.24-bookworm, which had stopped working once go.mod moved to Go 1.25 (go: go.mod requires go >= 1.25.0 (running go 1.24.13; GOTOOLCHAIN=local)); OVERCAST_GO_IMAGE still overrides

  • [docs] the Tier 0 / Tier 1 (inert) / Tier 2 operation-level vocabulary is documented in CONTRIBUTING.md alongside the existing service-level tiers, and the inert-tier rollout plan's status header, capability census and backfill table now match the code after phases I0–I2 landed — including the two premises (#1369 classification, #1373 error selection) that failed re-measurement and gate phase I3

  • [ec2] all 69 of EC2's registered typed operations now answer from the typed operation registry, which the service had registered but never dispatched to. Each shares one body with the legacy handler rather than carrying a second implementation that had fallen behind it — the typed twins of the twenty-one Describe* operations decoded no filters at all, so every one of them would have answered with the whole region had it ever been reached. Twelve mutations whose typed body validated a parameter and returned success without touching the store (TerminateInstances, StartInstances, StopInstances, DeleteTags, DeleteVpcEndpoints, ModifyInstanceAttribute, the four Authorize/Revoke``SecurityGroup*, ModifySubnetAttribute, ModifyVpcAttribute) now perform the write, and five creates that dropped part of their input (RunInstances' security groups and tags, CreateVpc's DNS-support default, CreateSubnet/CreateSecurityGroup's lifecycle events, CreateTags) now carry all of it. DeleteVpc/DeleteSubnet/DeleteSecurityGroup run their dependency checks on both dispatch paths, so a DependencyViolation answer is the same whichever one served the request. Two differential tests — one for reads, one for mutations, seeded on two independent handlers since a mutation cannot be replayed against the same store — hold every routed operation to the same bytes and the same store effect as its legacy twin. No response changes for an existing caller: legacy answered these calls before and answers the identical bytes now

  • [ecs] a task's awslogs output is followed with reconnect, exact de-duplication of the lines a timestamp-based resume replays, and batched CloudWatch writes, so a broken daemon connection no longer silently ends a task's logs; event timestamps are the container's own, not the host's arrival time

  • [lambda] a function's X-Amz-Log-Result tail now holds exactly that invocation's own lines, and its CloudWatch stream is ordered START / handler output / END / REPORT exactly, on every invoke rather than only on tail-requesting ones. The function's stdout and stderr are now owned by an init process inside the container — the same process model AWS uses, where RAPID is the parent of the runtime and serves the Runtime API on 127.0.0.1:9001. Because the init reads every line itself, it knows which invocation was in flight when the line was written, and it drains both pipes before it forwards the runtime's response; Overcast is told where an invocation's output ends instead of inferring it from Docker's log stream going quiet. docker logs on a function container is unchanged — the init tees every line to the container's own stdout and stderr byte for byte — with the init's own [overcast-init] diagnostics interleaved on stderr, which never reach CloudWatch, the tail or the Telemetry API. Inside the container AWS_LAMBDA_RUNTIME_API now carries the AWS value 127.0.0.1:9001, which is the init's proxy; the per-execution-environment endpoint on the host is passed separately as OVERCAST_RUNTIME_API and is read only by the init. Nothing outside the container changes: the Runtime API, the extensions API, the Telemetry/Logs API, LAMBDA_RUNTIME_API_PORT and every response shape are as they were. The init is delivered in a named volume seeded once per Overcast process rather than copied into each container, so a cold start is within ~10 ms of what it was; its CPU comes out of the function's allocation, as AWS's own init does, which is visible as occasional ~80 ms stalls when a 128 MB function is driven with no think time at all (see docs/services/lambda.md)

  • [lambda] platform records reach Telemetry/Logs API subscribers as the event AWS documents — the event's own type (platform.start, platform.report, …) and an object record — instead of a "platform"-typed event whose record was the log line as a string. CloudWatch output and X-Amz-Log-Result tails are unchanged in both log formats.

  • [server] cleartext HTTP/2 (h2c) is served through the standard library's http.Server.Protocols instead of the deprecated x/net/http2/h2c wrapper; prior-knowledge HTTP/2 and HTTP/1.1 behave as before, and the rarely used Upgrade: h2c handshake now continues on HTTP/1.1

  • [web/ci] the console's server-side TypeScript types are generated from the Go response structs by cmd/tsgen into web/src/types/api.gen.ts, and make check-ts fails CI when the two drift — a Go field rename can no longer ship a console reading undefined. Health, metrics, debug metrics, advisories, the event stream and diagnostics provenance came first; the Map page's topology graph, the Inbox's captured messages and the request-tracing payloads (trace detail, list, count and search) have since joined them, and their hand-written mirrors are gone. trace.Entry/Hop now marshal through package-level wire structs that cmd/tsgen renders — the generator will not render a type that has its own MarshalJSON, so a struct can no longer be described as a shape it never sends

  • [web] every colour in the web UI now resolves through a design-system token. Service identity, map node and startup-phase colours move onto the ten-slot categorical ramp (--cat-1..10); state colours collapse onto --success/--warning/--danger/--accent; the debug JSON viewer adopts the shared syntax tokens. Each token carries its own light and dark value, so the 429 raw Tailwind hue classes that rendered one fixed colour on both themes are gone, and a test fails the build on the next one

  • [web] the SES identity and API Gateway API-key delete confirmations now use the shared dialog, adding the "esc to cancel" keyboard hint

  • [web/iam] the Groups tab's expanded member list renders in its own panel below the group table instead of as an extra row inserted under the clicked group — the shared list-table kernel (ResourceTable) has no row-expansion concept, and this is the same below-the-table pattern API Gateway's usage-plan key list already used

  • [web] resource list tables sort by clicking a column header, and hide columns from a Columns menu. The wave covers CloudFront's six lists (Distributions, Continuous Deployment Policies, Field-Level Encryption Configs and Profiles, Key Groups, Realtime Log Configs); the Lambda function and layer lists, the ECS cluster list, the Auto Scaling group list and the Applications list — by name and by the counts and timestamps each page carries (memory, timeout, running tasks, capacity, latest version, created); DynamoDB, RDS, ElastiCache, EFS, S3 and ECR; API Gateway's REST and HTTP API lists, the WAF Web ACL list and the Cognito user pool list; CloudFormation, CloudWatch Logs, SSM, Step Functions and Secrets Manager; and SQS, SNS, Kinesis, MSK and Pipes — name on every page, plus message counts and visibility timeout on Queues, shards and retention on Streams, brokers on Clusters and creation time on Pipes. The sort is deep-linkable as ?sort=name / ?sort=-name (or ?sort=id, ?sort=-created) on the KMS, SSM, AppSync, Step Functions, CloudFront, DynamoDB, RDS, ElastiCache, EFS, S3, ECR, WAF and Cognito pages and on the CloudFormation Stacks and CloudWatch Log Groups index pages, alongside the filter's q — so a sorted view survives a reload and can be shared. Distributions, the ECS cluster and Auto Scaling group lists, RDS, ElastiCache, EFS, Log Groups and Queues also gain the Columns menu for their wider tables. Moving the API Gateway, WAF and Cognito lists onto ResourceTable is what gives them a real loading skeleton, a load-error state and a delete confirmation identical to every other list in the UI

  • [web] the tables inside detail pages sort the same way, and now show the app's skeleton rows while loading and its standard empty state when there is nothing to list, instead of a bare spinner or a "None defined." line that meant in-flight, empty and failed alike. Covered: the Lambda layer, ECS cluster, Applications, EC2 instance and EC2 VPC pages (versions, tasks, services, container instances, subnets, route tables, endpoints, security groups and tags); a distribution's Origins, Origin Groups and Invalidations; the ECR images table and DynamoDB's secondary-index tables; API Gateway's routes, integrations, stages, deployments and authorizers, AppSync's data sources, resolvers, functions, API keys and schema types, and Cognito's users and group members; a stack's parameters, outputs, tags and resources, a log group's streams, an SSM parameter's version history, a state machine's executions and a secret's rotation versions — the last three now newest-first rather than trusting the order the API returned them in; and the SQS queue, SNS topic, Kinesis stream, Pipes pipe and EventBridge bus sub-tables, where the bus page's rule list also tells "nothing here yet" apart from "nothing matches your filter". The REST API's resource tree and the Cognito user's attribute grid stay hand-rolled: the first expands a row into per-method rows, the second is an editable name/value grid rather than a resource list

  • [web] the last hand-built attribute grids — Applications, CloudFront's distribution configuration and monitoring panels, the ECS primary-deployment summary, WAF's Web ACL detail and Cognito's pool, app-client and user rows — render through the shared definition table, so every detail page's field labels, value typography and unset-field em dashes match

Fixed

  • BREAKING [acm/apigateway/appregistry/appsync/cloudformation/cloudfront/cognito/dynamodb/ecr/efs/glue/iam/kinesis/kms/msk/secretsmanager/stepfunctions] every one of the seventeen services that had a tag-accepting operation reaching AWS's own tag constraints in name only now routes it through the shared serviceutil.ValidateTags — a reserved aws: key prefix, an over-length key or value, or more than 50 tags on one resource is refused instead of silently stored, on every TagResource-style operation and every Create*'s inline Tags member these services carry. Three of the seventeen (Cognito, Glue, Step Functions) were already fixed and needed no change
    migration: fix any tags on these services that use a reserved aws: key prefix, exceed the 128/256-character key/value length limit, or exceed 50 tags on one resource — Overcast previously accepted and silently stored these; AWS itself has always rejected them

  • BREAKING [backup] BackupVaultTags/CreateBackupVault and BackupPlanTags/CreateBackupPlan are now validated and stored instead of being silently accepted and dropped, so a vault or plan tagged with a reserved aws: key prefix, an over-length key or value, or more than 50 tags is refused instead of succeeding with the tags discarded
    migration: fix any BackupVaultTags/BackupPlanTags that use a reserved aws: key prefix, exceed the 128/256-character key/value length limit, or exceed 50 tags on one resource — Overcast previously accepted and silently dropped these; AWS itself has always rejected them

  • BREAKING [cloudformation/ecr] ECR repository templates now get their ImageTagMutability, ImageScanningConfiguration, EncryptionConfiguration and Tags. The repository handler's create sent only repositoryName and applied RepositoryPolicyText/LifecyclePolicy as follow-ups, dropping the rest even though the ECR service already stored and echoed ImageTagMutability on both wire paths — a CDK repository with imageTagMutability: TagMutability.IMMUTABLE provisioned as mutable. ImageTagMutability and ImageScanningConfiguration now reach CreateRepository on create and are reconciled through two new operations, PutImageTagMutability and PutImageScanningConfiguration, on update; Tags reaches CreateRepository and is reconciled through TagResource/UntagResource, the same as the Lambda, LogGroup, Secrets Manager and SQS handlers. EncryptionConfiguration is forwarded on create and, since real ECR has no API to change it later, a template that changes it now replaces the repository instead of an update silently keeping the old encryption. EmptyOnDelete is honoured on delete: force is only passed to DeleteRepository when the template says EmptyOnDelete: true, matching real CloudFormation's default of failing to delete a repository that still holds images
    migration: a stack relying on the old always-force delete must set EmptyOnDelete: true on the repository to keep deleting it while it still holds images

  • BREAKING [cloudformation/elasticache] AWS::ElastiCache::CacheCluster takes its cluster name from ClusterName, the resource's own property. It read CacheClusterId — what the API calls the parameter, and not a property the resource has at all — so a template that named its cluster got one named after the stack instead, and every endpoint the stack advertised belonged to a cluster nobody had asked for. An unnamed cluster now gets CloudFormation's generated name rather than <stack>-cache, which carried neither the logical ID nor the random suffix: two unnamed clusters in one stack were handed the same ID, and the second create failed on a name the template never mentions. The generated name is lowercased and capped at 50 characters, which is what ElastiCache accepts in a cluster ID
    migration: CacheClusterId on this resource was never valid CloudFormation and is now ignored. Spell it ClusterName to keep the cluster's name; leave it and the next change to that resource replaces the cluster with the generated one. Stacks already deployed keep the physical ID they were given until something replaces the resource

  • BREAKING [cloudformation/elasticache] Fn::GetAtt on a cache cluster resolves the endpoint pair the engine has — RedisEndpoint.Address/.Port for Redis and Valkey, ConfigurationEndpoint.Address/.Port for Memcached, as AWS populates them. Only the ConfigurationEndpoint pair was exported, whatever the engine, and a GetAtt on an attribute a handler does not export does not fail: it falls back to the physical ID. CDK's attrRedisEndpointAddress therefore handed out a bare cluster ID as a hostname, which no container advertises and nothing can resolve, so an ECS task with REDIS_HOST baked in at deploy time died on getaddrinfo rather than reaching the cache. The pair the engine does not have resolves empty, so a template reading the wrong one gets the same nothing it would get on AWS
    migration: a Redis or Valkey cluster's ConfigurationEndpoint.Address is now empty rather than carrying the endpoint, which is what AWS answers for it. Read RedisEndpoint.Address — CDK's attrRedisEndpointAddress — as code written against AWS already does

  • BREAKING [cloudformation] a DELETE_COMPLETE stack is no longer resolvable by name on DescribeStacks, DescribeStackEvents, ListStackResources, DescribeStackResources, GetTemplate and GetTemplateSummary — each now answers ValidationError: Stack with id <name> does not exist, matching AWS's rule that a deleted stack is addressable only by its unique stack ID once its name is free to reuse. The stack ID (ARN) captured at create still resolves the deleted stack's final state and events, as does a stack mid-DELETE_IN_PROGRESS looked up by name; only the terminal record excludes the name path. Mutations already treated DELETE_COMPLETE as nonexistent; this closes the one read-side divergence that was deliberately left open
    migration: a client that polled DescribeStacks by name to observe a delete finishing (rather than by the stack ID CreateStack returned) now gets a ValidationError instead of a DELETE_COMPLETE body once the delete completes — the same as the AWS SDK's stack-delete-complete waiter, which already treats that error as the terminal success case. Poll by the stack's ID, or treat the does-not-exist error as "deleted" the way the waiter does

  • BREAKING [cloudformation/cloudtrail] AWS::CloudTrail::Trail now reads the trail name from TrailName, the real resource schema's property, instead of Name
    migration: rename Name to TrailName in any template using this resource; a template still setting Name gets an auto-generated trail name instead of a validation error, since Overcast has no per-property unrecognised-property diagnostic

  • BREAKING [cloudformation/kms] AWS::KMS::Key used to provision successfully and quietly keep the AWS default whenever a template set Origin to anything other than AWS_KMS, MultiRegion: true, or EnableKeyRotation: true — the emulator models none of import/CloudHSM origins, the replica-key relationship multi-Region implies, or rotation state. CreateKey now answers ValidationException for the first two (and both are replacement-required on update); EnableKeyRotation: true (CDK's enableKeyRotation, a common setting) is now dispatched to the KMS service's EnableKeyRotation operation, which does not exist, so the resource fails loudly instead of silently reporting rotation off forever — the same "dispatch to the gap" treatment AWS::IAM::User's LoginProfile gets
    migration: drop Origin/MultiRegion from any AWS::KMS::Key template that relied on them being ignored, and drop EnableKeyRotation: true (enableKeyRotation: true in CDK) until key rotation is emulated

  • BREAKING [cloudformation/athena] AWS::Athena::WorkGroup now reads the workgroup configuration from WorkGroupConfiguration, the real resource schema's property, instead of Configuration (the CreateWorkGroup API member it maps onto); a template's result location, bytes cutoff and the rest were silently dropped before
    migration: rename Configuration to WorkGroupConfiguration in any template using this resource; a template still setting Configuration gets a workgroup with no configuration instead of a validation error, since Overcast has no per-property unrecognised-property diagnostic

  • BREAKING [config] native overcast serve now defaults to binding 127.0.0.1 instead of 0.0.0.0; the containerised default (Docker image, OVERCAST_DATA_DIR_SOURCE=image) is unchanged at 0.0.0.0. An explicit OVERCAST_LISTEN always wins over either default
    migration: native users reaching Overcast from another machine, a VM, or a phone on the same network must set OVERCAST_LISTEN=0.0.0.0 to restore the old reach

  • BREAKING [config] OVERCAST_HOST is renamed to OVERCAST_LISTEN (LocalStack's GATEWAY_LISTEN idiom) and removed rather than kept as an alias — a leftover OVERCAST_HOST now fails at startup naming OVERCAST_LISTEN as the replacement, instead of being silently ignored
    migration: rename OVERCAST_HOST to OVERCAST_LISTEN wherever it is set (env, .env files, docker-compose.yml, CI config) — the value format is unchanged, the same comma-separated address list

  • BREAKING [ec2] DeleteSecurityGroup, DeleteSubnet and DeleteVpc now answer DependencyViolation while a dependent (instance, ENI, subnet, gateway, endpoint, peering connection) remains, matching AWS instead of deleting unconditionally; the VPC default security group answers CannotDelete
    migration: clear dependents before deleting a security group, subnet or VPC — a template or script that relied on the unconditional delete now sees the same refusal real AWS gives, surfaced by CloudFormation teardown as DELETE_FAILED instead of a silent dangling reference

  • BREAKING [pipes] CreatePipe/UpdatePipe now refuses a stream source's DeadLetterConfig naming a destination that is not an SQS queue or an SNS topic, instead of storing it and only failing once a batch exhausts its retries
    migration: point DeadLetterConfig.Arn at an SQS queue or SNS topic, or remove it

  • BREAKING [rds] CreateDBCluster and CreateDBInstance enforce the identifier shape AWS documents — 1 to 63 letters, numbers or hyphens, starting with a letter, no trailing or doubled hyphen — and store identifiers lowercase, so a describe finds what a create made whatever case it was sent in
    migration: an identifier real RDS would have refused is now refused here too; rename it to match the documented shape. A mixed-case identifier keeps working and comes back lowercased, as it does on AWS

  • BREAKING [rds] a DBSubnetGroupName naming no existing subnet group is refused by CreateDBCluster and CreateDBInstance, and DBSubnetGroupNotFoundFault carries AWS's documented 404 rather than a 400
    migration: create the subnet group before the database that references it, as AWS requires. Code branching on the 400 status rather than the error code should read the code, which has not changed

  • BREAKING [rds] cluster BackupRetentionPeriod defaults to 1 and is held to AWS's documented 1–35 on create and modify; Port is held to 1150–65535 on both
    migration: a cluster created without BackupRetentionPeriod now reports 1 rather than 0. BackupRetentionPeriod=0 was never a value AWS accepts for a cluster and is now refused; send 1 to mean the minimum

  • BREAKING [router/appregistry/efs/eks/scheduler] AppRegistry, EFS, EKS and Scheduler no longer answer POST / for an X-Amz-Target header (AppRegistry., EFS., EKS., Scheduler.) or a Smithy RPC v2 label (/service/<name>/operation/<op>) — a wire the pinned models never gave these restJson1 services and no real SDK ever sends. Both doors now answer the same honest "unknown operation"/"unsupported protocol" error any nonsense request gets, never a 200
    migration: address these four services only at their modeled REST bindings, the paths already documented under docs/services/, the same way any AWS SDK or the aws CLI reaches them today

  • [appsync] AppSync's tag validation no longer rejects a digit in a tag key — a locally re-implemented validator predating the shared one was more restrictive than AWS's own documented tag-key pattern and is retired in favour of it

  • [athena/cognito/ec2/ecs/elasticache/eventbridge/firehose/rds/ssm/stepfunctions] create-time tags are no longer dropped. Ten services accepted a Tags member on create and silently discarded it, so a resource had to be tagged again afterwards to be tagged at all: CreateWorkGroup (Athena), CreateUserPool (Cognito, whose request modeled no UserPoolTags member at all), CreateCluster/CreateService (ECS), CreateReplicationGroup (ElastiCache), CreateEventBus/PutRule (EventBridge, where PutRule merges with whatever the rule already carries, as AWS documents), CreateDeliveryStream (Firehose), CreateDBInstance/CreateDBCluster/CreateDBSubnetGroup (RDS), PutParameter (SSM) and CreateStateMachine (Step Functions, whose request modeled no tags member either). EC2's TagSpecification.N was honoured only by RunInstances, CreateNatGateway and CreateVpnGateway, and now also by CreateVpc, CreateSubnet, CreateSecurityGroup, CreateInternetGateway, CreateRouteTable, CreateVpcEndpoint, CreateVpcPeeringConnection, CreateNetworkInterface and CreateKeyPair — the endpoint and peering-connection responses previously carried no tag set at all and now do. Tags are validated and stored where the tag operations already look, so ListTagsForResource sees them without a follow-up call; SSM applies them only when the call creates a new parameter, matching AWS's rule that Tags cannot update an existing one. ECS's RegisterTaskDefinition and ElastiCache's CreateCacheCluster/CreateServerlessCache already stored theirs and needed no change, and ElastiCache's independent CBOR typed path is fixed alongside its Query/XML handler. Everywhere else the legacy handler now delegates to its typed counterpart instead of duplicating it, which also fixed a pre-existing Step Functions bug: the typed path minted state machine ARNs with the account default region rather than the request's actual region

  • [cloudformation] API Gateway templates now get the stage variables, authorizers and models they declare. AWS::ApiGateway::Stage's Variables and AWS::ApiGatewayV2::Stage's StageVariables used to be parsed into nothing on create — the service already read stage.Variables into a Lambda proxy integration's stageVariables at execution time and already accepted the field on update, but create never sent it, so every integration behind a CDK-declared stage variable saw an empty map. Both now reach the stage on create, and update patches per-key changes in place. AWS::ApiGateway::Method's AuthorizerId and AWS::ApiGatewayV2::Route's AuthorizerId round-trip the same way — a method or route wired to a CfnAuthorizer no longer loses the association. Method.RequestParameters and MethodResponses (via PutMethodResponse) are forwarded on create, and AWS::ApiGateway::RestApi's Policy, Tags, BinaryMediaTypes and DisableExecuteApiEndpoint round-trip on create (the two scalars also on update) — GetRestApi previously never surfaced Policy back either, even though CreateRestApi already stored it. AWS::ApiGateway::Authorizer, AWS::ApiGateway::Model and AWS::ApiGateway::RequestValidator are provisioned resource types now instead of falling through to the unknown-type stub; the apigateway service already implemented all three, nothing in CloudFormation's handler registry ever dispatched to them. RestApi.Body/BodyS3Location and ApiGatewayV2::Api.Body (OpenAPI import) fail the resource loudly instead of silently provisioning a routeless API, since neither is implemented. Remaining gaps — Method.RequestModels/RequestValidatorId/OperationName/AuthorizationScopes, Stage.MethodSettings/TracingEnabled/cache and logging settings, RestApi.MinimumCompressionSize/ApiKeySourceType, and the equivalent ApiGatewayV2::Integration/Route/Stage/Api properties — are documented in docs/services/cloudformation.md rather than wired or failed, since the service has no field for any of them yet

  • [cloudformation] a cancelled stack update keeps the resource records it already applied. The shutdown exit in the update walk discarded the accumulated records on failStack, so the persisted UPDATE_FAILED stack described a pre-update state that no longer matched the service-side resources — and a retried update replayed resources that had already changed. The branch now retains the records the same way the DisableRollback failure branches do: applied resources keep their attempted state, untouched ones keep their prior record, and a subsequent update picks up where the cancelled one stopped

  • [cloudformation] a custom resource's properties now reject a secure dynamic reference ({{resolve:secretsmanager:...}}, {{resolve:ssm-secure:...}}) instead of resolving it into the Lambda's request — AWS documents secure values as unsupported in custom resources, for the same reason AWS::CloudFormation::Init and EC2 UserData carry the restriction: a custom resource's Lambda may log its own event payload. A plain {{resolve:ssm:...}} reference still resolves. OldResourceProperties is now actually threaded through to the Lambda (it was previously discarded and effectively sent empty), in the same form as ResourceProperties, so an update whose properties did not change no longer looks like every property changed

  • [cloudformation] a teardown now tells the truth about what it did and did not delete. Around fifty resource handlers discarded the result of their delete and reported success unconditionally, so a rollback recorded DELETE_COMPLETE and the stack reached ROLLBACK_COMPLETE with the resource still standing; they now report the failure, and the stack reaches ROLLBACK_FAILED with the resource left in its list, which is the signal an operator needs and what real CloudFormation does. That was only safe once absence stopped counting as failure: around sixty-five handlers — API Gateway, AppRegistry, AppSync, EC2, ECS, EFS, EKS, EventBridge, KMS, Lambda, MSK, Pipes, Route 53, Step Functions, S3 and WAFv2 among them — returned their delete error unfiltered, so a rollback failed over a resource nobody needed deleted, and they now recognise the absent-resource answer their service gives. The two together let DeleteStack stop swallowing genuine delete failures: a resource still standing leaves the stack DELETE_FAILED with the resource listed, as on AWS, so a retry knows what is out there, while a resource that is merely already gone can never wedge a stack. EC2's DependencyViolation routes through the same deletion-blocked sentinel as IAM's DeleteConflict, RDS's deletion protection and a non-empty S3 bucket — DeleteInternetGateway, the main-table case of DeleteRouteTable and DeleteVpnGateway all refuse while something is still attached, and that refusal is now marked as the lasting condition it is rather than reported as an ordinary, possibly transient, delete failure

  • [cloudformation] rollback honours DeletionPolicy: Retain, and RetainExceptOnCreate is honoured on CreateStack, UpdateStack and ExecuteChangeSet rather than only on RollbackStack. An update rollback no longer deletes the resources that update created regardless of the policy, so a failed update no longer destroys a resource the template asked to keep; the replacement a failed update created is still deleted, since the original is what the stack rolls back to. RetainExceptOnCreate on the operation opts back into deleting a retained resource, so a create rollback that asks for it deletes what create made — including resources marked DeletionPolicy: Retain — instead of orphaning them for the next deploy to collide with

  • [cloudformation] ELBv2 templates now get the target-group health check, TargetType and Scheme they declare. AWS::ElasticLoadBalancingV2::TargetGroup's TargetType was stored and echoed by the elbv2 service (instance vs ip vs lambda matters for the Fargate path specifically, since an awsvpc service registers ip targets) but CreateTargetGroup's Query request never carried it, so every target group came back as the instance default regardless of what the template asked for. The whole HealthCheck* family (Protocol, Port, Path, Enabled, IntervalSeconds, TimeoutSeconds, HealthyThresholdCount, UnhealthyThresholdCount), Matcher, ProtocolVersion and IpAddressType had the same gap — parsed out of the template and dropped before the request left the provisioner. All of it now reaches CreateTargetGroup, defaults the way the real API documents when the template leaves it unset, and reconciles through a new ModifyTargetGroup on update; TargetType, Protocol, Port and VpcId still force replacement, matching AWS. TargetGroupAttributes (e.g. deregistration_delay.timeout_seconds) reaches a target group through two new operations, ModifyTargetGroupAttributes and DescribeTargetGroupAttributes — the real CreateTargetGroup has no field for attributes either, so the provisioner makes the follow-up call CDK's synthesized template implies. AWS::ElasticLoadBalancingV2::LoadBalancer's Scheme had the matching gap: stored and echoed by the service, never sent by CreateLoadBalancer, so internal vs internet-facing never round-tripped from a template; IpAddressType had the same fix. Tags on both CreateTargetGroup and CreateLoadBalancer are threaded directly, as the real API accepts them inline on create. AWS::ElasticLoadBalancingV2::Listener's DefaultActions now forwards each action's RedirectConfig and FixedResponseConfig wholesale instead of only Type and TargetGroupArn — a CDK HTTP→HTTPS redirect listener no longer loses its target on the way to the emulator, though the proxy still only forwards traffic and does not act on a redirect or fixed-response action's data-plane behaviour. Health checks and the two listener action configs are round-trip-faithful, not enforced: DescribeTargetHealth still always reports "healthy". Remaining gaps — weighted ForwardConfig, Certificates/SslPolicy/AlpnPolicy/MutualAuthentication, the Cognito/OIDC auth actions, SubnetMappings, and AWS::ElasticLoadBalancingV2::ListenerRule (no handler registered at all) — are documented in docs/services/elb.md rather than wired or failed, since the service has no field for any of them yet

  • [cloudformation] IAM templates now get the permissions they declare. AWS::IAM::Role used to send exactly three parameters to CreateRole and drop everything else, so a CDK role built with addToPolicy(...) or addManagedPolicy(...) provisioned successfully with no permissions at all. The role handler now dispatches AttachRolePolicy per ManagedPolicyArns entry and PutRolePolicy per Policies entry, passes Path, Description, MaxSessionDuration, PermissionsBoundary and Tags through to CreateRole, and reconciles adds and removes on update — compensating already-applied mutations when a later one fails, so a rejected update leaves the role as it was. AWS::IAM::User gains the same treatment for Groups, ManagedPolicyArns, Policies, PermissionsBoundary and Tags; AWS::IAM::ManagedPolicy honours its Description and attaches to the principals its Roles/Users/Groups lists name; AWS::IAM::Policy writes its document onto groups and users on create, not just roles; and AWS::IAM::Group is now a real resource rather than an unknown-type stub. Every create-side attachment has a matching detach in the same handler's teardown, so a stack that creates the attachment can also delete itself now that IAM enforces DeleteConflict — out-of-band relationships are never swept, and a refused delete restores the detachments it made. A LoginProfile fails the resource loudly instead of being silently dropped, since IAM does not emulate login profiles

  • [cloudformation] Ref of a CommaDelimitedList (or List<...>) parameter now resolves to a list, as on AWS, with an empty value resolving to the empty list. CDK's bootstrap template depends on this: the CLI passes the execution policy ARN as a CommaDelimitedList whose Ref feeds the toolkit role's ManagedPolicyArns directly

  • [cloudformation] AWS::DynamoDB::Table now applies Tags on create and reconciles additions, changes and removals through TagResource/UntagResource on update, and reconciles GlobalSecondaryIndexes add, remove and throughput changes through GlobalSecondaryIndexUpdates on update instead of forwarding indexes at create only. AWS::DynamoDB::GlobalTable is no longer a no-op stub: Overcast emulates a single region, so it provisions the Replicas entry naming the deploying stack's own region as a real table, forcing NEW_AND_OLD_IMAGES streaming and defaulting to on-demand billing, and fails the stack loudly rather than completing silently when no Replicas entry names that region or PROVISIONED billing is requested

  • [cloudformation] AWS::Kinesis::Stream no longer drops RetentionPeriodHours, Tags, StreamEncryption or StreamModeDetails. RetentionPeriodHours threads through IncreaseStreamRetentionPeriod/DecreaseStreamRetentionPeriod on create and reconciles on update, so a CDK stream with retentionPeriod: Duration.days(7) no longer reports Kinesis' 24-hour default; Tags reconcile through AddTagsToStream/RemoveTagsFromStream; and StreamModeDetails and StreamEncryption are new Kinesis fields, threaded at create, reconciled through the new UpdateStreamMode/StartStreamEncryption/StopStreamEncryption operations, and round-tripping through DescribeStream/DescribeStreamSummary. ShardCount changes still force replacement, since UpdateShardCount is not implemented (#1096); an unnamed stream now gets a generated per-instance name so that replacement does not collide with the still-live original

  • [cloudformation] AWS::SSM::Parameter no longer drops Description, Tags, Tier, DataType, AllowedPattern or Policies between CDK and the emulator. The handler's create and update used to send only Name, Type and Value to PutParameter; they now thread every scalar through and reconcile Tags through SSM's own AddTagsToResource/RemoveTagsFromResource on create and update. Tier, DataType and Policies were dropped on the SSM service side too: PutParameter now stores them and GetParameter/DescribeParameters/GetParameterHistory echo the stored value instead of a hardcoded Standard/text/[]

  • [cloudformation] SQS queue templates now get their Tags and RedriveAllowPolicy. The queue-attribute mapping forwarded twelve queue attributes and RedrivePolicy but silently dropped both — the service already supported each (TagQueue/ListQueueTags/UntagQueue, and RedriveAllowPolicy as a validated queue attribute), CloudFormation just never called them. Tags now reaches CreateQueue on create and is reconciled through TagQueue/UntagQueue on update — added, changed and removed keys, plus stack-level propagated tags, the same as the Lambda, LogGroup and Secrets Manager handlers. RedriveAllowPolicy is forwarded alongside RedrivePolicy on create and update and round-trips through GetQueueAttributes; its redrivePermission restriction is validated for shape but, as before, not enforced against StartMessageMoveTask or automatic DLQ redrive. AWS::SQS::QueuePolicy remains a stub, tracked separately

  • [cloudformation] a generated physical ID no longer contains two consecutive hyphens when truncation lands on a separator, which RDS and several other services refuse

  • [cloudformation] AWS::Scheduler::Schedule templates no longer lose Description, ScheduleExpressionTimezone, StartDate, EndDate or KmsKeyArn. CreateSchedule sent Name, GroupName, ScheduleExpression, State, FlexibleTimeWindow and Target while the scheduler service already stored and returned every one of the five — ScheduleExpressionTimezone and StartDate/EndDate are also honoured by the firing dispatcher — so a CDK schedule with a non-UTC timezone or a future start date deployed clean and fired on the wrong clock. Update also always forced full replacement; it now calls UpdateSchedule in place, and only Name/GroupName changes still replace, matching the resource's create-only properties on real AWS. AWS::Scheduler::ScheduleGroup (Name, Tags) had no gap

  • [cloudformation] AWS::Logs::LogGroup's RetentionInDays is coerced from a JSON string to a number before it reaches the Logs service, matching real CloudFormation's own type coercion. A String-typed Ref or Parameter renders the property as "7" rather than 7 — CloudFormation accepts either form, but the emulator's Logs handler decodes a strict int and the raw string failed there instead of being accepted, on both CreateStack and an in-place UpdateStack. A value that still is not a valid retention number, string or not, continues to fail with the Logs service's own InvalidParameterException rather than a decode error

  • [cloudformation] AWS::EC2::VPC's EnableDnsSupport/EnableDnsHostnames and AWS::EC2::Subnet's MapPublicIpOnLaunch are threaded through to the emulator instead of being parsed into nothing. A CDK Vpc construct sets both DNS attributes on every VPC it synthesizes, and MapPublicIpOnLaunch on every public subnet — none of the three reached the emulator's ModifyVpcAttribute/ModifySubnetAttribute calls, and even a direct call was metadata-only: DescribeVpcAttribute always answered true regardless of what was set, and DescribeSubnets always answered mapPublicIpOnLaunch=false. All three are now real store fields: CreateVpc defaults to EnableDnsSupport=true/EnableDnsHostnames=false (matching AWS), the seeded default VPC and its default subnets get the account-default-VPC values (both DNS attributes and MapPublicIpOnLaunch all true), ModifyVpcAttribute/ModifySubnetAttribute persist whatever is set, and DescribeVpcAttribute/DescribeSubnets report the stored value. The provisioner issues the follow-up Modify*Attribute call on create, and both AWS::EC2::VPC and AWS::EC2::Subnet gained update support so a template that only changes these properties (or tags) updates in place instead of replacing the resource — CidrBlock/InstanceTenancy on a VPC and VpcId/CidrBlock/AvailabilityZone on a subnet still force replacement, matching real CloudFormation. AWS::EC2::EIP also stops hardcoding Domain: "vpc" and reads it (and Tags) from the template; DescribeAddresses now returns the tags it is given. What reads these values downstream — ECS/RDS/Lambda-VPC routing behaviour keyed off EnableDnsHostnames and the rest — is separate and tracked in #1100; this closes the storage, round-trip and template-threading gap, not the data-plane consumption of it. The other catalogued gaps remain: AWS::EC2::Subnet's Ipv6CidrBlock/AssignIpv6AddressOnCreation/EnableDns64/PrivateDnsNameOptionsOnLaunch/OutpostArn, AWS::EC2::VPC's Ipv4IpamPoolId/Ipv4NetmaskLength, AWS::EC2::NatGateway's ConnectivityType/PrivateIpAddress, and AWS::EC2::Route's IPv6/ENI/TGW/peering/carrier-gateway destinations

  • [cloudformation] ElastiCache replication groups now get the engine they declared. AWS::ElastiCache::ReplicationGroup's CreateReplicationGroup request dropped Engine and EngineVersion entirely — the service already read both and already treated an Engine change as replacement on update, but create never sent them, so the engine image fell through to its redis:7 default regardless of the template. A replication group declared for Valkey or Memcached, or pinned to a specific Redis version, silently started the wrong Docker container behind an endpoint that looked healthy. SnapshotRetentionLimit and PrimaryClusterId are forwarded on create too, and DescribeReplicationGroups now echoes Engine, matching AWS. AWS::ElastiCache::CacheCluster's PreferredAvailabilityZone and CacheParameterGroupName — both already stored fields — and inline Tags now round-trip from create as well. Remaining gaps on both resource types — Port, security-group and encryption properties, LogDeliveryConfigurations, replication-group node-group sizing, and a handful of others the emulator has no field for yet — are documented in docs/services/elasticache.md rather than silently accepted

  • [cloudformation/secretsmanager/stepfunctions] a filter value outside the enum AWS models is now rejected instead of silently matching nothing. ListStacks rejects a StackStatusFilter value outside AWS's full StackStatus enum with ValidationError — the complete 23-value enum is modelled, including the IMPORT_* states Overcast itself never produces, so a caller legitimately filtering on one of those is still accepted. ListSecrets and BatchGetSecretValue reject a Filters entry whose Key is outside AWS's enum with InvalidParameterException, and ListExecutions rejects a statusFilter value outside the ExecutionStatus enum with ValidationException — each of them one of the operation's own declared errors

  • [cloudformation] a stack-tag-only update now reconciles the tags it should. AWS::DynamoDB::Table, AWS::SSM::Parameter and AWS::StepFunctions::StateMachine left their tags unreconciled when only the stack's tags changed and the resource's own template properties did not, even though each resource's own tag-reconciliation code was already correct. AWS::Transfer::Server no longer forces replacement when only its Tags change either; it reconciles them through TagResource/UntagResource in place, the same as every other resource type that reconciles tags without replacing

  • [cloudformation] Cognito user pool and app client templates now get their AliasAttributes, DeviceConfiguration, AccountRecoverySetting, SmsConfiguration, SmsAuthenticationMessage, SmsVerificationMessage, EmailVerificationMessage, EmailVerificationSubject, LambdaConfig, PreventUserExistenceErrors, ReadAttributes and WriteAttributes. The user pool handler forwarded only a core subset of AWS::Cognito::UserPool properties and had no update handler at all, so any template change replaced the pool instead of reconciling it; the app client handler had the same update gap. The Cognito service now stores and returns all of the properties above; the CloudFormation handlers thread them on create and reconcile the mutable ones on update through UpdateUserPool/UpdateUserPoolClient, replacing the pool only when AliasAttributes or UsernameAttributes change, since neither is a member of real Cognito's UpdateUserPoolRequest. LambdaConfig round-trips through DescribeUserPool, so a pool's configured triggers are visible — and, as of this release, invoked

  • [cloudformation] AWS::KMS::Key stack deletion carries the template's PendingWindowInDays through to ScheduleKeyDeletion instead of the hardcoded 7 days it used before, falling back to KMS's own documented 30-day default when the property is absent. Tags round-trips too: CreateKey accepts and stores them, merged with any stack-level tags, where it previously dropped the property entirely, and update reconciles changes through TagResource/UntagResource, the same add/remove diff every other tag-aware handler here uses. KeyPolicy and Enabled were already threaded on create; AWS::KMS::Alias has no properties beyond AliasName/TargetKeyId and had no gap

  • [cloudformation] AWS::StepFunctions::StateMachine templates no longer lose DefinitionSubstitutions, LoggingConfiguration, TracingConfiguration or Tags. The handler's create sent only name, definition, roleArn and type, so a CDK state machine built from DefinitionBody.fromString/fromFile with definitionSubstitutions provisioned with its ASL ${key} placeholders unresolved — inert today, but a state machine executed under the ASL interpreter would fail at the first task state on the literal placeholder string. DefinitionSubstitutions is now applied on both create and update: a matched ${key} is replaced with its value before the definition is stored, and an unmatched placeholder is left verbatim rather than failing the deploy, since CloudFormation does not validate substitution completeness against the definition. LoggingConfiguration and TracingConfiguration are threaded on create as well as update, and Tags are applied through TagResource on create and reconciled on update. A StateMachineType change on update was silently ignored instead of forcing replacement — AWS documents it Update requires: Replacement and does not support changing it in place — so it now returns the same replacement-required path StateMachineName already took

  • [cloudwatch] GetMetricData now answers over the JSON protocol (X-Amz-Target: GraniteServiceVersion20100801.GetMetricData), the pinned model's primary protocol for the operation — previously only the Query protocol reached it, so an AWS SDK speaking JSON got UnknownOperationException

  • [docker/ecs] a container-backed launch no longer fails outright on a transient registry error. The shared image puller retries a rate limit ("too many requests"), a registry 5xx, and a deadline, timeout or reset while a pull was in flight, within one bounded 3-attempt backoff — at most 4 attempts, about 2.5 s in total — closing the window where ECS's RunTask and every other container-backed create reported failure for a blip that would have cleared a moment later. A caller's own context ending, whether shutdown or its own deadline, still stops the pull immediately rather than retrying

  • [docs] the published documentation now says what the emulator actually does. The CloudFormation resource-type coverage docs state the real number — 132 registered types at the time of writing, 136 after this release's additions, not "~50" — and docs/cdk.md lists every handler. The LocalStack migration guide's known-gaps table, stale in every row (SigV4 validation, CloudWatch metrics, Firehose, Route 53 and ElastiCache are all implemented), is replaced by a pointer to the generated service index. The configuration reference gains the ~30 environment variables it omitted, including the OVERCAST_MCP_REMOTE_EXPOSURE/OVERCAST_MCP_AUTH_TOKEN remote-access controls. OVERCAST_SIGV4_VALIDATE's startup log and configuration-reference row claimed SigV4 validation was "not yet implemented — all requests are accepted" while the middleware was validating signatures and answering a bad one with 403 InvalidSignatureException, actively misdirecting an operator away from the real cause of signature failures; both now describe what the flag does, including where signing secrets come from (IAM user access keys and STS session credentials, with the local-dev test fallback), and validation behaviour itself is unchanged. docs/operation-manifest.md no longer silently drops S3, Lambda, API Gateway, CloudFront, Pipes, CloudWatch and seven other REST/legacy-dispatch services: it lists every excluded service by name with a derived reason, and explains how its typed-dispatch counts differ from the service index's capability-registry counts. The web console section no longer describes a separate BFF server process that could fail to start — the console is served in-process by the Go binary, and OVERCAST_UI_PORT=0 / --ui-port 0 is how to turn it off. A code comment that wrongly equated OVERCAST_HOST with LocalStack's LOCALSTACK_HOST is corrected: the true analogue is OVERCAST_HOSTNAME

  • [dynamodb] table responses carry the identity and the shape AWS documents. CreateTable, DescribeTable, UpdateTable and DeleteTable now include TableDescription/Table.TableId — a UUID minted once at CreateTable time and persisted with the table, so it stays stable across reads and restarts — and tooling that keys off a table's identity rather than its name or ARN (CloudFormation's Fn::GetAtt TableId, for one) no longer gets an empty string with no indication anything was missing. DeleteTable's response returns the deleted table's description under TableDescription, with TableStatus reported as DELETING, matching the other three and AWS's documented shape; it previously reused the DescribeTable-shaped Table wrapper, which strict SDK decoders would silently drop since the member name did not match

  • [ec2] the typed dispatch path answers as the legacy one did. Typed-operation errors render EC2's <Errors><Error> envelope rather than the generic Query one, restoring the wrapper deleted when typed dispatch was switched off, and AllocateAddress's typed body no longer ignores Domain and always allocates a vpc-domain address — found by the new differential test rather than by a caller, since the typed path had never been reachable

  • [ec2] CreateSecurityGroup returns the tagSet its result shape has always carried. Every other EC2 create nests its tag set inside the resource element it answers with; this one puts it at the top level next to groupId, and Overcast omitted it, so a caller that tagged a group at create had to issue a DescribeSecurityGroups to read back what it had just set.

  • [ec2] AllocateAddress honours create-time TagSpecification.N on both dispatch paths. Neither ever parsed it — the call succeeded and the Elastic IP came back untagged, invisible to DescribeTags and to the tagSet DescribeAddresses renders — and a tag the model refuses, such as a reserved aws: key, now fails the allocation instead of allocating and dropping the tag. Per the AWS model, AllocateAddressResult itself carries no tag set; the tags surface through the describes.

  • [ecr] CreateRepository now marks its response with x-overcast-emulation-limitation whenever the repositoryUri it just minted does not name a registry proven to be listening. No ECR registry container running at all, and one whose container started but the daemon-reachability probe never confirmed it answers, both used to be a Warn log line only — CreateRepository still returned 200 with a repositoryUri that a later docker push answers with 405 Method Not Allowed, or that an ECS or Lambda image pull fails against, with nothing tying that failure back to the repository that caused it. Both wire paths (the AWS JSON 1.1 handler and the RPC v2 CBOR typed operation) mark the same sentence; inside a CloudFormation deploy it becomes the resource's ResourceStatusReason, next to the AWS::ECR::Repository it is about. Any typed operation's response type can now opt into this channel without new plumbing per service

  • [ecs] containers in an awsvpc task now share one network namespace, so 127.0.0.1 reaches every container in the task as it does on Fargate — the ECS sidecar pattern (nginx to php-fpm, an application to its X-Ray daemon) works against the AWS-correct task definition, and the address DescribeTasks reports is the one every container answers on

  • [ecs] StopTask over RPC v2 CBOR records the stop before it kills the containers, as the JSON path already did. Killing them first let the Docker die event file the task as EssentialContainerExited instead of UserInitiated, charge its deployment a failed task, and schedule a service replacement nobody asked for. The two paths now share one implementation, so the CBOR one also takes a stopped task out of its service's target groups

  • [ecs] a task that fails to start part-way tears down the containers it had already started, instead of leaving them holding ports and memory until shutdown

  • [iam] UpdateRole is now supported (Description, MaxSessionDuration; an empty description clears it, an omitted one is left alone), CreateRole/GetRole round-trip Description and MaxSessionDuration, CreatePolicy/GetPolicy round-trip Description, and CreatePolicyVersion with SetAsDefault=true replaces the operative document and bumps DefaultVersionId — superseded versions are not retained

  • [inithooks] a hook script's timeout on Windows now kills its whole process tree, not just the shell. TerminateProcess previously reached only the shell, so a backgrounded descendant (sleep 5 &, say) was orphaned and kept running, still holding whatever handle it had inherited — including, under go test, the test binary's own stdout pipe, which reds the pre-push gate after every test has passed. Unix already killed the whole process group on timeout; Windows now does too, through a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE

  • [lambda] a LogType: Tail invocation no longer loses the handler's output when the Docker daemon is busy. The tail used to be assembled from two sources that shared no clock and no sequence — the Runtime API response on one channel, the container's bytes read back off the daemon on another — so "this invocation's output has all arrived" could only be inferred from silence plus a bound, and a daemon under load could beat any bound chosen. Widening that bound is what the interim fixes did, and each closed a real case: a cold start whose log stream took a daemon round trip to connect, a warm invocation whose already-open reader was slow to deliver under contention, and a wait whose bound ran from the moment it began rather than from the last observed pipeline event (connect answered, reader parked, bytes read, line appended), with one absolute 1 s ceiling so a wedged pipeline could never hold up the response. The tail is now assembled from a numbered stream the container itself produces, and the invocation's response names the last line that belongs to it — which retires the guesswork rather than tuning it, and with it the whole class of defect

  • [lambda/ecs] a line a function or task printed is no longer dropped when the daemon wrote it out of timestamp order — which it does whenever stdout and stderr are written close together, since each stream has its own copier: a Python handler's first print regularly lost to a sys.stderr write that landed first, and the line went missing from both CloudWatch Logs and the X-Amz-Log-Result tail. The follower's reconnect de-duplication treated "older than the newest line seen" as "already delivered" on every connection; it now does so only for the replay a reconnect asks for, up to the watermark it resumed from. A task's awslogs output has followed the same path since the shared follower landed and is fixed with it — and after a reconnect, the lines a traceback prints within one nanosecond are no longer dropped past the first

  • [lambda/pipes] a partial-batch failure report is honoured rather than refused. A Lambda event source mapping accepts FunctionResponseTypes: ["ReportBatchItemFailures"] — validated against AWS's one-member enum instead of being stored unread — and a function that answers with batchItemFailures has exactly the records it named retried: an SQS mapping deletes only the succeeded messages and leaves the reported ones to become visible again, and a DynamoDB Streams mapping retries from the earliest reported sequence number, as AWS checkpoints an ordered stream. AWS's edge cases come with it: an empty or absent list is a complete success, and a response that cannot be read (invalid JSON, a missing, empty or non-string itemIdentifier, an identifier naming a record that was not in the batch) is a complete failure that acknowledges nothing and logs why. A pipe's Lambda target reports the same way for SQS, Kinesis and DynamoDB Streams sources — an SQS source deletes only the messages the target did not name, a Kinesis source rewinds its shard cursor to just before the earliest reported record, and a DynamoDB Streams source retries from that record onwards — but a pipe has no opt-in flag, so a response that never mentions the member is left alone and a target returning an arbitrary payload behaves as before. A Step Functions target and a Lambda enrichment still do not report, and both are documented as such

  • [lambda] the layer version metadata endpoint behind the Lambda layer detail page was missing from the Go BFF entirely and 404d in every build, not only under pnpm dev; it is proxied like every other Lambda route now

  • [lambda] a handler error reported through the Runtime API now comes back as FunctionError: Unhandled, as AWS reports it for an exception thrown by any current runtime. It used to be Handled unless the runtime reported Runtime.ExitError — a value no modern runtime produces for a thrown error, and one that broke SDK callers branching on the documented value. The new compat InvokeWithError test is what surfaced it

  • [lambda] an execution environment survives exactly as long as its function does. Deleting a function now reaches the containers it is still starting: an environment between docker create and its first GET /next belonged to nothing the pool could see — not the warm set, not the reservation, not the instance tracker — so DeleteFunction walked past it and the container went on running with no function to serve, until the invocation that started it eventually gave up. Every creation is now registered for exactly that window, so deleting the function aborts it and an environment that comes up anyway is destroyed rather than handed out; a create abandoned mid-flight, whose container the daemon finishes after the answer is lost, is removed by the name the cold start gave it. Conversely, a function deleted and created again under the same name now keeps the environments built for it: deletion marks the name so that an invocation still in flight destroys its environment on release instead of pooling it for a function that no longer exists, but the mark was only lifted by the next invocation — so provisioned concurrency configured on the recreated function had every environment it built destroyed on release, leaving the reservation unfilled until something invoked the function, and proactive initialization lost its environment the same way. Configuring a reservation or initializing ahead of traffic now counts as proof the function is in use, as an invocation always did

  • [lambda] a cold start no longer has its CPU cut by another environment of the same function finishing INIT. The INIT burst — the ~2 CPUs a container gets while it initializes, before dropping to its memory's proportional share — was tracked per function rather than per execution environment, so when a function had two environments initializing at once (two event source mappings on one stream deliver to one function at the same instant, and every concurrent invocation cold starts its own), the second displaced the first. The environment that finished INIT threw the throttle-down at whichever container had registered last, cutting a container still in INIT to a fraction of a core — a one-second init becoming half a minute on a 128 MB function — while the environment that had actually finished kept burst CPU for the rest of its life. Both now belong to the container that reported INIT complete

  • [lambda] bind-mount hot reload now picks up an edit to a file the runtime has already loaded, which is the gesture the feature exists for. A hot-reload function's execution environment was keyed on the mounted directory's own mtime, and a directory's mtime moves only when an entry is created, deleted or renamed inside it — so editing index.js in place left the identity untouched, the warm container was reused, and Node's require cache (or Python's sys.modules) kept serving the module that container had loaded. Saving under a new name, deleting a file, or cdk watch all worked, which is why this went unnoticed. The identity is now a fingerprint of the whole mounted tree — every entry's path, and every file's size and modification time — so an in-place edit retires the warm environment through the same path UpdateFunctionCode uses and the next invoke cold starts against the edited source. On a Windows host the identity never moved at all, for either reason: the tree was being read through the Docker-form path (/c/Users/you/app), which names nothing on Windows, so both this check and the TypeScript-source warning silently did nothing there. The fingerprint is bounded so that recomputing it before each invocation does not become the cost of invoking, and docs/services/lambda.md states each bound exactly: node_modules, .git, __pycache__, .venv, .mypy_cache and .pytest_cache are fingerprinted by name and not descended into (which is what keeps a 30,000-file dependency tree at the same 1–2 ms as the source beside it); the walk stops at 20,000 entries and 24 directory levels; and a read costing over 25 ms is rate-limited to 20× what it cost, capped at 2 s, so a tree behind a slow Docker Desktop file share costs a bounded fraction of the invoke path rather than half a second on every invocation, while everything under the budget — a few thousand files on a local disk — is re-read before every invocation and is exact. Two boundaries are honest rather than fixed: a same-length edit made within a filesystem's 1–2 s timestamp granularity of the previous one may be seen a change late, and Overcast running in a container can only fingerprint a source directory that is mounted into Overcast itself — it now logs a warning naming the path when it cannot read one, instead of reloading nothing and saying nothing.

  • [lifecycle/eks/stepfunctions] three shutdown races of the same shape are fenced. lifecycle.Scheduler.After (the readiness health-check retries MSK, RDS, ElastiCache and EFS run) is now a defined no-op once Stop has run, so a still-running health check stops rescheduling instead of racing shutdown; EKS's CreateCluster live-mode bootstrap called liveWg.Add(1) without first checking the stopping flag under its lifecycle mutex, so a create racing Stop could leave a k3s container starting after shutdown, and a create that lands after Stop has begun is now refused, leaving the cluster record FAILED with a reason instead of a half-started container; and Step Functions' startExecution — the single choke point StartExecution, StartSyncExecution and a nested states:startExecution all funnel through — registered its run and called wg.Add with nothing synchronising either against Stop's wg.Wait, so a stopping flag guarded by the same lock now fences the reservation: Stop sets it before it ever waits, and an execution arriving concurrently either reserves its slot strictly before that happens or gets a defined ServiceUnavailableException with no goroutine launched and no wg touched, its already-persisted RUNNING record unwound to ABORTED rather than left stuck. All three could surface under -race on unrelated PRs

  • [pipes] a Kinesis source's MaximumRetryAttempts and DeadLetterConfig are honoured, matching the retry-then-dead-letter behaviour DynamoDB Streams sources already had — a batch that exhausts its retries now reaches the configured SQS queue or SNS topic instead of being retried on every poll tick forever

  • [protocol] a request member typed as an open document (CloudWatch's PutMetricAlarm Metrics and EvaluationCriteria are the ones in reach today) decodes from CBOR the same way it decodes from JSON. CBOR maps landing in such a member used to decode with untyped keys, which the emulator's own JSON persistence cannot serialise, so the call failed with a bare 500 InternalError on the CBOR door and succeeded on every other. A CBOR map with non-string keys is now rejected at the decode instead, which is the honest answer — no Smithy structure or document has them

  • [rds] Aurora cluster endpoints now resolve from other containers, and carry a port the caller can dial. Endpoint and ReaderEndpoint were registered nowhere in Docker's DNS, so a name ending in a split-horizon domain fell through to public DNS and answered loopback — an ECS task or Lambda reading cluster.clusterEndpoint.hostname from a CDK stack dialled itself and got a refused connection, and only the per-instance endpoint worked. The writer member's engine container now carries both cluster names alongside its own, and DescribeDBClusters renders the address and port from that instance on the same per-caller rules DescribeDBInstances has always used, so a host caller is given the published port instead of the engine port nothing binds on the host. The reader endpoint resolves to the writer: Overcast's cluster members do not share storage, so spreading reads over the replicas would answer from an empty database — see docs/services/rds.md

  • [rds] deleting an Aurora DB instance now removes it from its cluster, and deleting the writer promotes a survivor. DBClusterMembers was only ever appended to, so DeleteDBInstance dropped the instance record and left the membership entry behind: DescribeDBClusters went on reporting a member that no longer existed, and if the deleted member was the writer the cluster was left with no writer at all — which took its endpoints with it, since the cluster's address, port and DNS all answer from whichever member holds that role. A surviving member is now promoted the way AWS chooses one during a failover, by promotion tier, and the cluster endpoint names move onto the new writer's engine container. Moving them detaches and re-attaches that container, so connections held open to it over that network drop, exactly as a failover on AWS drops them. A cluster whose last member is deleted keeps its AWS-shaped endpoint names and its own port, unchanged

  • [rds] StopDBCluster and StartDBCluster answer in their own response envelopes instead of CreateDBClusterResponse, so an AWS SDK can deserialize them. The state change was always correct, so nothing that read the stored record noticed

  • [router/backup/apigateway] a /tags/{resourceArn} request whose ARN belongs to a service whose tag operations are not implemented answers the generated 501 with the x-emulator-unsupported marker. API Gateway's ARN-keyed tag store was the dispatcher's fallback for every ARN no other service claimed, so aws backup list-tags read HTTP 200 {"tags":{}} from a service the caller never addressed — and Backup implements no tag operations at all. The store still answers its own ARNs and AppRegistry's servicecatalog ones, whose SDK shares the endpoint

  • [router] a request signed for one service (OpenSearch or MSK, say) that lands on a path modeled for a different real AWS service now gets that service's own scoped-credential 403 — InvalidSignatureException naming the expected signing name, matching what AWS answers when a credential scope names the wrong endpoint — instead of falling into S3's bucket/object wildcard and returning a nonsensical NoSuchBucket/NoSuchKey. Unsigned traffic, S3's own traffic, and a scope that is not a real AWS signing name are unaffected

  • [s3/sns] S3 bucket notifications now deliver to SNS topics. TopicConfigurations was accepted, stored and returned, but the notification dispatcher never read it, so a configured S3 → SNS destination silently never fired — a success response, a correct-looking round trip, and no delivery. Matching events now publish to the named topic through SNS's own fan-out, so the whole S3 → SNS → SQS/Lambda/email path works; event-type selection and prefix/suffix filters share the same matcher as the SQS and Lambda destinations. The envelope matches real S3: the {"Records":[…]} JSON travels as the SNS notification's Message string with subject "Amazon S3 Notification", so a queue subscribed to the topic receives the standard SNS envelope whose Message parses back into the S3 event

  • [scheduler] KmsKeyArn is now accepted on CreateSchedule/UpdateSchedule and round-trips through GetSchedule, matching FlexibleTimeWindow and ScheduleExpressionTimezone: stored and returned, but nothing is actually encrypted with it

  • [sns] Publish matches AWS's documented behaviour on several fronts reviewed against the API reference. It accepts exactly one of TopicArn, PhoneNumber or TargetArn as the destination, where TopicArn was previously required even for a PhoneNumber/TargetArn call; PhoneNumber delivers directly to the Inbox as SMS with no topic involved; TargetArn (mobile platform endpoints) returns an explicit 400 InvalidParameter in place of a misleading "missing TopicArn" error; MessageStructure=json is implemented, selecting each subscriber's own protocol-keyed payload (falling back to default) for the envelope, raw delivery, email and SMS bodies alike; Message (256 KB) and Subject (under 100 characters, no line breaks or control characters) are validated on both Publish and each PublishBatch entry; and a topic whose name ends in .fifo gets its MessageGroupId/MessageDeduplicationId (or ContentBasedDeduplication) parameters checked on Publish — a call that was never valid against real AWS now fails the same way, while full FIFO ordering, deduplication and SequenceNumber generation remain unimplemented and documented as such

  • [sns] a subscription whose delivery dependency was never wired into this instance (SQS enqueuer, Lambda invoker, SMTP mailer, SMS sender, or outbound webhook/push capture) no longer vanishes silently on Publish — it now fails the same way any other delivery failure does: NumberOfNotificationsFailed is recorded, the subscription's dead-letter queue receives it when a RedrivePolicy is set, and a one-time warning per topic and protocol is logged naming the missing wiring

  • [sqs] DeleteQueue, PurgeQueue, SetQueueAttributes, TagQueue and UntagQueue no longer emit an empty <{Action}Result></{Action}Result> wrapper on the legacy Query/XML protocol. These five operations have no modeled output at all, and real AWS's Query/XML response for them goes straight from the opening <{Action}Response> element to <ResponseMetadata> with no result element in between. The JSON protocol was already correct ({} with no wrapper) and is unaffected

  • [state] HybridStore.Set/Delete/DeletePrefix no longer pin TierCached writes (S3 objects, SQS messages, Lambda deployment packages, CloudWatch Logs events, …) permanently in memory; they now live only in the pending overlay until flushed, then in SQLite

  • [stepfunctions] UpdateStateMachine is implemented — the CloudFormation update path called it unconditionally whenever a mutable property was present, which is effectively always since DefinitionString is rarely omitted, so every in-place stack update to a state machine's mutable properties was quietly falling back to full replacement (AWSStepFunctions.UpdateStateMachine had no handler). DescribeStateMachine now echoes loggingConfiguration/tracingConfiguration, defaulting to AWS's documented OFF/disabled shape when unset, on both the legacy JSON and typed CBOR paths

  • [sts] AssumeRole and AssumeRoleWithWebIdentity no longer repeat RoleSessionName in both segments of AssumedRoleUser.Arn. The first segment is now the role name parsed out of the request's RoleArn, matching AWS's arn:aws:sts::<account>:assumed-role/<RoleName>/<SessionName> shape — fixed identically on the legacy Query XML and the typed JSON/CBOR paths

  • [web] global search now hits the query cache instead of refetching on every keystroke — the S3, SQS, SNS, Kinesis, Lambda, Secrets Manager and Logs contributors built their cache-lookup key as [service, resource, baseUrl] (Logs omitted the endpoint entirely), which never matched the [baseUrl, region, service, resource] shape the real feature queries use, so the lookup always missed and every search ignored the selected region

  • [web] a log group with no streams, or one whose streams failed to load, says so instead of rendering an empty table; the same page's stream list gains the standard skeleton while it loads. A secret whose rotation returned no versions no longer renders a headers-only table

Removed

  • [web/ci] the dead Node BFF — the standalone entry point for the dev Hono server (web/api/src/node.ts), the @overcast/api package manifest around it (web/api/package.json, its own pnpm-lock.yaml, the dev/build/start scripts and @hono/node-server), and the "BFF server" bundle (pnpm run build:server, web/api/src/server.ts) that CI built on every push. All of it was a leftover from before the web console moved into the Go binary: nothing installed, built or started the entry point, and no image ever copied or started the bundle. web/api/src is typechecked, tested and resolved from web/ as plain source files

Release: https://github.com/Neaox/overcast/releases/tag/v0.0.1-alpha.37