Skip to content

v0.0.1-alpha.26

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Jul 01:56
9ea8e82

Overcast v0.0.1-alpha.26

Docker Images

Full image with the web management console:

  • Pull: ghcr.io/neaox/overcast:0.0.1-alpha.26
  • Channel 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.26
  • Channel 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.26
docker run --rm -p 4566:4566 ghcr.io/neaox/overcast-slim:0.0.1-alpha.26

Native Binaries

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

Asset SHA256
overcast-darwin-amd64 6ac8c65defc1d343242f685f47909e47695b80b31c9796c9b0db623acdf0a4b8
overcast-darwin-arm64 db81a245b0e3c19ea3d166369eec25351d814c6287fd200456c4dfcf74e5a51b
overcast-linux-amd64 a77c1142aeeee065fdc3e24ad553f490c61e2cb6ee7792b864a16eb1ddec8af3
overcast-linux-arm64 0c7c7ddb128fba194c27f9df1332fea2978a8f241bbe41c7f9ddf7db7d832905
overcast-windows-amd64.exe bb7626ad41a9884e751267e3893ebd9ca41227d404fad3233a40c4513aef9326
overcastd-darwin-amd64 519c45f17baf1a51002dcb14b9f4d5ad26e4a9d9663868a2f84f514fe9e62b2f
overcastd-darwin-arm64 9897333aa399166c1c50839bce9840f6d4b4a244ea7bbfc0d95282c82a1713b3
overcastd-linux-amd64 93b89bf81d1a0c41bfd3fff198acdae7522d85c3a57c03a85a2cee9768109ccf
overcastd-linux-arm64 a90e7ad1baa6da0460910463c9ddf931937192d24d2df6c6986e70406c753bf8
overcastd-windows-amd64.exe 4c10dec6cbc0ee6d47ef255183504eadb8d34f9164900319c3819e09eee5f50e

Release Notes

Added

  • CI (release candidates) — every build of a release PR now publishes the exact images CI built to GHCR as ghcr.io/neaox/overcast[-slim]:<version>-rc.<n> (linux/amd64, <n> auto-increments per build, earlier RCs stay pullable), uploads all ten cross-built native binaries as workflow artifacts, and maintains a single release-notes-shaped bot comment on the PR with pull commands, image digests, and the artifact table. Until now the PR workflow built images and binaries only as checks and discarded them, so pre-release smoke testing always ran against local rebuilds rather than the candidate bits themselves. Release-candidate detection is one shared predicate rather than a branch-name convention: a same-repo PR whose VERSION carries no v<VERSION> tag yet (scripts/release-candidate-check.sh) — which also covers follow-up PRs after a failed release workflow, a case a release/* branch test is blind to; the release workflow's changelog validation now uses the same predicate and so gains the same coverage. Fork PRs and ordinary PRs are unaffected. The ten-asset binary matrix, previously duplicated between the CI and release workflows and drifting one edit at a time, is now stated once in a shared workflow_call (build-binaries.yml) used by both — so the binaries checked on the release PR are built by the same definition that publishes them.

  • HTTPS / HTTP/2 (OVERCAST_TLS=auto, overcast https) — Overcast can now serve both the API and the web UI over browser-trusted HTTPS with certificates minted from a local overcast CA, unlocking HTTP/2 in browsers. This fixes the web console starving under load: browsers cap HTTP/1.1 at six connections per origin (localhost included) and never negotiate cleartext HTTP/2, so the console's SSE feed, Lambda invoke progress streams, S3 transfers and polling could exhaust the sockets and make navigation hang; over TLS the browser multiplexes everything on one connection via ALPN. New overcast https enable|disable|status does the whole setup in one command (create the CA under <data dir>/ca, install it into the system trust store — user-scope certificate store on Windows, login keychain on macOS, system CA bundle on Linux — and mint a leaf covering localhost, 127.0.0.1, ::1, localhost.overcast.sh, *.localhost.overcast.sh, *.s3.localhost.overcast.sh, the other wildcard DNS domains, OVERCAST_HOSTNAME, and OVERCAST_SPLIT_HORIZON_HOSTS); overcast trust install|uninstall|status, previously a stub on every platform, now actually manages the CA in the trust store. OVERCAST_TLS=auto makes overcast serve mint/reuse that leaf at startup (re-minted automatically when the name set changes or expiry nears; leaves live 825 days, the CA 10 years, and re-minting never invalidates the trust-store install). Explicit OVERCAST_TLS_CERT/OVERCAST_TLS_KEY now applies to the web UI listener too, not just the API; the SPA bootstrap, minted client-facing URLs, and init-hook environment (AWS_ENDPOINT_URL, plus a new AWS_CA_BUNDLE) all say https when TLS is on; the Docker HEALTHCHECK probes https as a fallback; plain-HTTP defaults are unchanged, including h2c for SDK clients. serve --bridge is skipped with a warning while TLS is on (the port-80 proxy speaks plain HTTP). See the new docs/https.md. Docker setup is now two commands with no shared volume: the daemon serves its CA certificate (public half only) at GET /_overcast/ca.pem (404 until a CA exists; mirrored on the web UI as /api/ca.pem), and overcast https enable --endpoint http://localhost:4566 fetches it, validates it actually is a CA certificate, caches it under <data dir>/ca-remote/<host_port>/ (kept separate from the local CA so status/disable --endpoint find exactly what was installed), and installs it into the system trust store — http:// spellings auto-negotiate to a TLS-only daemon, non-loopback endpoints are refused without an explicit --trust-remote acknowledgement, and the same --endpoint works on overcast trust install|status|uninstall. A containerized daemon logs the exact --endpoint command at startup. Both Docker images serve TLS + HTTP/2 (the slim image on its API listener; both health checks handle https), and on Linux installed anchors are now fingerprint-named so a local CA and fetched daemon CAs coexist (anchors installed by earlier builds under the old fixed name are still recognised and cleaned up).

  • Lambda (concurrency) — a function now scales out to one execution environment per concurrent invocation instead of sharing a single warm container, and provisioned concurrency actually allocates environments. PutProvisionedConcurrencyConfig pre-initializes the requested number in the background (IN_PROGRESSREADY), holds them open against the idle sweep, replenishes them when one is lost, rebuilds them against the new configuration after a code/config update, restores them across a restart, and marks their containers AWS_LAMBDA_INITIALIZATION_TYPE=provisioned-concurrency; Allocated/Available are reported from the environments that actually exist. It behaves as a floor rather than a ceiling, so invocations beyond the reservation spill over into on-demand capacity with a cold start rather than throttling, matching AWS. Previously the config was stored, echoed back as READY, and nothing was ever pre-warmed. DeleteProvisionedConcurrencyConfig and ListProvisionedConcurrencyConfigs are now implemented, and all four operations moved to the /2019-09-30/ path AWS actually serves them on — the AWS SDKs' calls previously missed the handlers entirely and fell through to the S3 catch-all. Reason: FAILED with a StatusReason is now reported when Docker is unavailable, instead of claiming capacity that cannot exist.

  • Lambda (concurrency limits)ReservedConcurrentExecutions is now enforced: exceeding it returns AWS's 429 TooManyRequestsException (Type/message/Reason/retryAfterSeconds body, X-Amzn-Errortype and Retry-After headers) with Reason: ReservedFunctionConcurrentInvocationLimitExceeded, so throttle-handling code and the "reserve 0 to disable a function" idiom now work as they do on AWS; it was previously stored and returned but never applied. The three reserved-concurrency operations also moved to the API versions AWS actually serves them on — PutFunctionConcurrency and DeleteFunctionConcurrency on /2017-10-31/, GetFunctionConcurrency on /2019-09-30/ — having been registered on Lambda's /2015-03-31/ base, where no AWS SDK or CLI call could reach them: they fell through to the S3 catch-all and came back as 404 NoSuchBucket naming the API version as the bucket, so reserved concurrency could not be set at all through a real client. GetFunctionCodeSigningConfig had the same defect and moved to /2020-06-30/. A disabled Lambda now also reports "service disabled" on every API version it serves rather than letting the non-base ones fall through to S3. New LAMBDA_MAX_INSTANCES, LAMBDA_MAX_INSTANCES_PER_FUNCTION and LAMBDA_MAX_WARM_INSTANCES (default 10) bound how many containers Overcast will run on your machine; when unset, the first two — along with LAMBDA_DOCKER_MAX_CONCURRENT_STARTS — are sized to the machine Docker actually runs containers on (its GET /info NCPU/MemTotal, which is the Docker Desktop VM or remote daemon, not necessarily where the Overcast process runs): concurrent starts clamp(NCPU/2, 2, 8) because each start bursts ~2 CPUs during INIT, instances clamp(MemTotal×0.65 / 256 MiB, 4, 32), per-function clamp(maxInstances/2, 2, maxInstances), logged once at startup, with the previous fixed defaults (4/25/10) applying when /info cannot be read. New LAMBDA_MAX_MEMORY_MB additionally budgets memory in bytes rather than containers: a new container is admitted only while Σ MemorySize of live containers stays inside the budget (default 65% of the Docker host's MemTotal; unlimited when /info cannot be read) — a real bound, since every container is hard-capped at its MemorySize with swap disabled — using the same reclaim → queue → throttle-at-timeout ladder, warning once (naming the env var) when first exhausted, and preferring to destroy rather than keep warm a just-finished container while the pool sits above ~90% of the budget; provisioned environments are never taken and running invocations are never interrupted. The pressure is visible at the default log level without per-container spam: entering the high-water regime warns once (budget, reserved, threshold — expect cold starts), recovering logs once with how many containers were shed and for how long, and a memory-bound idle-container reclaim warns where the routine count-cap reclaim stays at info — an invocation that cannot get one reclaims the least-recently-used idle container (never a provisioned one), then queues, and is throttled with Reason: ConcurrentInvocationLimitExceeded only if it is still waiting when the function's timeout expires. Asynchronous invocations are never throttled back to the caller (they were already answered 202) and are retried internally instead; a throttled event source mapping batch is left in flight so its messages return to the queue on the visibility timeout. Account-wide quotas and requests-per-second limits remain out of scope.

  • Lambda (code signing) — the code signing configuration a function is created with is now stored and read back. GetFunctionCodeSigningConfig was a stub that reported "no config" for every function, and nothing accepted or stored a CodeSigningConfigArn anywhere, so a CDK Function with codeSigningConfig set had it silently dropped. CreateFunction now accepts the ARN, CloudFormation passes the property through so the association survives a deploy, and PutFunctionCodeSigningConfig / DeleteFunctionCodeSigningConfig are implemented on the /2020-06-30/ path AWS serves them on — they previously fell through to the S3 catch-all and returned NoSuchBucket. The ARN is validated against AWS's documented pattern. Code signing is optional and that path is unchanged: a function without a configuration still answers ResourceNotFoundException, which is what AWS's own model requires, since CodeSigningConfigArn is a required member of the success response and so "no config" cannot be expressed as one. The configuration itself is now a real resource: CreateCodeSigningConfig, Get, Update, Delete, ListCodeSigningConfigs and ListFunctionsByCodeSigningConfig are implemented on /2020-04-22/, along with the AWS::Lambda::CodeSigningConfig CloudFormation resource type, so a CDK stack that declares a CodeSigningConfig and points a Function at it deploys and reads back. AllowedPublishers is required and UntrustedArtifactOnDeployment defaults to Warn, both as AWS models them; a function naming a configuration that does not exist is rejected with CodeSigningConfigNotFoundException, and deleting one still referenced by a function returns ResourceConflictException. Signature validation itself is deliberately not emulated — Overcast is not a security boundary.

  • Web UI — the system map now shows a function's real concurrency: one card per execution environment rather than one per function, ordered so environments doing work appear first, with a header summary of how many are running/idle/provisioned, a "+N more · scroll" hint when the list overflows, and provisioned environments marked and shown as reserved rather than counting down an idle TTL they are exempt from.

Changed

  • Web UI (connection status) — losing the emulator is now reported by a persistent toast in the bottom-right corner instead of a bar across the top of the shell, and it says what the bar could not: which reconnect attempt is pending, the seconds until it runs, when the data on screen was last live, and that writes are paused until the connection returns. retry now fires the pending attempt immediately, view events goes to the one page that still reads without the emulator, and the return is announced by a toast of its own, so a reconnect that happened while you were reading something else leaves a trace rather than being a disappearance you had to catch. Reconnection is Overcast's own now, not the browser's: an EventSource retries a dropped connection on a schedule it never exposes, and that is the common case rather than an edge one — a stopped emulator is a network-level failure, which leaves the connection in CONNECTING, and only a bad response closes it outright — so nothing could say when the next attempt was due and "retry now" had nothing to pre-empt. The connection is now closed on the first error either way and every attempt is scheduled by the SharedWorker that owns it, so all open tabs count down to the same moment, and one backoff (1s, 2s, 4s, then 5s) replaces the copy the worker and its no-SharedWorker fallback each kept. Resuming is unaffected: the last event id already travelled as a query parameter for exactly the reconnects a browser does not manage for us, so nothing is replayed twice or missed. The toast shares one bottom-right dock with ordinary toasts — it holds the corner and transient ones stack above it — and the topbar's endpoint dot goes amber for a drop, matching the toast that explains it, rather than the grey it uses while a first connection is still outstanding.

  • Web UI (tabs and sidebar) — the browser tab now carries the icon of the service you are on, drawn from the same registry entry the sidebar renders and inked light or dark from the tab strip's colour scheme, so a wall of open Overcast tabs is readable at a glance; a run narrowed with OVERCAST_SERVICES starts with its enabled services pinned in the sidebar instead of an empty pinned list, and those defaults are only written to local storage once you actually pin, unpin, or reorder them; a pinned service that this run has switched off is dimmed rather than hidden and stays clickable, so its page can explain why it is disabled; and CloudWatch's sub-navigation lists Logs before Metrics.

  • OVERCAST_SERVICES removed — every service now runs, always; a leftover value is ignored rather than rejected, so an existing compose file or CI job keeps working. The variable never saved resources: services were constructed either way and their constructors are barred from store reads and I/O, so a disabled service cost about what an enabled idle one costs. What it did cost was 85 branches in the wiring, eight of them pairs — a Lambda function without logs ran but wrote no invocation logs, one without ec2 started with no VPC attachment, S3 notifications without sqs were never delivered. Those failure modes are now unreachable rather than merely documented. Calls to a service you did not ask for no longer return 503 ServiceDisabled, because there is no such state; the web console no longer dims services or seeds the sidebar from a narrowed run.

  • Build from sourcegit clone && go build ./... now works with only the Go toolchain: the docs search index (internal/docssearch/index.gen.go, web/src/docs-index.gen.ts) is committed rather than generated on every build, and a committed web/dist/.gitkeep keeps //go:embed all:web/dist resolving before the SPA is built. A binary compiled without the SPA serves the API normally and returns an explanatory 503 (naming make build-web) on the web UI port instead of a bare 500; Docker and release builds are unaffected and still assert a real SPA.

Fixed

  • TLS (containers) — Lambda functions and ECS tasks can now reach Overcast when it serves TLS. With OVERCAST_TLS enabled the API listener speaks only TLS, but containers were still handed http:// endpoints, so every SDK call from every function failed with Go's plaintext rejection ("Client sent an HTTP request to an HTTPS server") surfaced as SDK deserialization garbage. Container endpoints now follow the listener's scheme (containerendpoint.BaseURL, shared by both runtimes), the trust root — the local CA in auto mode, the operator's certificate chain in explicit mode — is injected into every container at /opt/overcast/ca.pem by the same CopyToContainer mechanism as function code (a dockerized Overcast has no host path to bind-mount), and AWS_CA_BUNDLE/NODE_EXTRA_CA_CERTS/SSL_CERT_FILE/REQUESTS_CA_BUNDLE point at it, covering the CLI, botocore, Go, Node, Ruby and python-requests stacks. The Java SDK reads only its own truststore — documented caveat in docs/https.md. Under TLS, environment/payload URL rewriting targets the SAN-covered hostname rather than the raw container address, which would trade a scheme error for a certificate error. Plain-HTTP deployments are unchanged: no CA material is injected and endpoints stay http.

  • Networking (client-facing URLs) / AppSync / API Gateway / Lambda / Cognito / CloudFormation — every URL Overcast mints now carries the port the caller reached it on, so it is dialable by the party it was handed to. With OVERCAST_HOSTNAME set and the API port remapped (docker run -p 4652:4566), AppSync's uris, API Gateway v2's apiEndpoint, Lambda function URLs and the Cognito issuer all carried the listen port and were undialable from the host — measured: SQS queue URLs said :4652 while AppSync said :4566 on the same instance. The repo had four hand-kept base-URL precedences (serviceutil, SQS, CloudFormation, Cognito's typed path) and two disagreed on the port; they now share one implementation, serviceutil.ClientBaseURLFromOrigin: configured hostname (authoritative, #351), the caller's port, and a TLS-aware scheme that upgrades and never downgrades. The Cognito issuer following the caller's port is what OIDC Discovery 1.0 §4.3 requires — issuer must equal the URL the configuration was retrieved from — and Overcast's own token validation is port-agnostic (the pool ID is read from the issuer path, guarded by test); the typed CBOR path used to mirror this precedence by hand and had drifted to the config port, so a caller's wire protocol changed their issuer — both paths now call the same function. SQS wire responses deliberately keep echoing the caller's exact origin (SDKs dial the QueueUrl itself; documented in code so a tidy-up cannot "unify" it away), and ECR's repositoryUri stays on the configured host:port because the docker daemon, not the API caller, dials it. containerendpoint correspondingly rewrites split-horizon hostnames carrying the published port to the listen port on the way into function and task environment and invoke payloads — the name resolves inside a container but that port is bound only on the host; the patterns are precomputed so the per-invoke miss path allocates nothing. Full design and per-service constraints: docs/plans/client-facing-url-minting.md; user-facing summary in docs/networking.md.

  • S3 (POST on an object)POST /{bucket}/{key} with no subresource now answers 405 MethodNotAllowed, as S3 does, instead of 501 NotImplemented. POST is only an operation on an object when a subresource selects one (?uploads, ?uploadId=, ?restore, ?select); without one there is no such AWS operation, so the two responses make different claims. NotImplemented — with its x-emulator-unsupported header — tells a client this emulator is incomplete and invites a workaround, for a request real S3 refuses too. That misdirection had a cost: it was the response surfaced when a CloudFront distribution routed a POST to an S3 origin, and it sent the investigation after a missing feature rather than a misrouted request. The subresource operations dispatch unchanged.

  • CloudFront (cache behaviors, origins) — two defects that sent a viewer request to the wrong origin, both silently. Path patterns now follow the documented CacheBehavior.PathPattern semantics: * matches across /, and the leading / is optional as AWS states ("CloudFront behavior is the same with or without"). The matcher special-cased a trailing * and otherwise used Go's path.Match, whose * stops at a /. So AWS's own *.jpg example matched nothing below the root, a wildcard in the middle (/api/*/detail) never matched, and — worst — a pattern written without a leading slash (api/*, jobs*) could never match anything at all, because a request path always has one. Such a behavior was dead: its requests fell through to the DEFAULT behavior and were served by whichever origin that named, with nothing logged to say so. Origin resolution now recognises any endpoint this emulator serves, by deferring to the same HostClassifier the router uses for inbound requests and dialling locally with the origin's own Host preserved. Only the {bucket}.s3.{...}.amazonaws.com spelling had been recognised, by a bespoke prefix check; an S3 website endpoint, a legacy dash-region bucket ({bucket}.s3-{region}.amazonaws.com), or an origin naming any non-S3 service fell through to "custom origin" and was dialled at its literal domain — so a distribution fronting an emulated service reached out to real AWS over the internet instead of being served locally. API Gateway, Lambda function URLs and AppSync origins work as a result, and a service becomes usable as an origin as soon as it becomes routable, rather than needing its own case here. Genuine third-party origins are unaffected and still dialled as configured.

  • CloudFront (origin groups) / Web UI — a cache behavior whose TargetOriginId names an origin group rather than an origin is now served. CreateDistribution has always accepted the shape — validateOriginRefs adds every OriginGroup Id to the set of legal targets, citing AWS's origin failover documentation — but the proxy resolved the target against Origins only, so every viewer request returned 502 Origin not found. CDK emits exactly this when a Distribution is given an origin group, so a stack could deploy clean and then fail on every request, with the distribution's own config offering no hint that the target was legal but unreachable. Group members are now tried in declared order, with failover on the response codes the group's FailoverCriteria lists and on a connection failure to the member. Failover is restricted to GET, HEAD and OPTIONS, as AWS restricts it — for any other method CloudFront returns the primary's response as-is, and a request whose body has already been streamed to the first origin cannot be replayed to the second anyway. A member naming an origin that does not exist is skipped rather than failing the lookup; a group with no resolvable member still reports Origin not found, so a genuinely broken distribution does not become a silent one. The console's distribution detail page gained an Origin Groups table alongside Origins, listing each group's primary, its failover members and the status codes that trigger them — the origin list on its own could not explain where a distribution's traffic actually goes.

  • API Gateway / AppSync / ECR (advertised URLs) — the URLs Overcast hands back now use the grammar AWS clients expect wherever the address they are minted on can carry them. The web console's REST v1 invoke URL — what its copy button yields, and what its test panel calls — was built path-style as {base}/restapis/{apiId}/{stage}/_user_request_{path}, a shape no AWS SDK produces and nothing outside Overcast understands, even on a deployment whose every subdomain resolves; it is now http://{apiId}.execute-api.{region}.{base}/{stage}{path}. AppSync's uris.GRAPHQL and uris.REALTIME were path-style for the same reason and are now host-routed too, which also fixes CloudFormation's Fn::GetAtt GraphQLUrl and RealtimeUrl, since those pass the value straight through. Both fall back to the path-style form — still served, so nothing holding one breaks — only when the base cannot carry a subdomain (a bare localhost, whose *.localhost does not resolve on Windows or macOS, or an IP literal); that condition is now one predicate shared by the server and the console rather than a per-service judgement, so OVERCAST_HOSTNAME=localhost.overcast.sh gets the AWS shape everywhere and a default install keeps the URL that works there. A custom domain's appsyncDomainName was minted as d-{hex}.appsync-api.{region}.amazonaws.com, a name that resolves to real AWS and can never reach the emulator — the same defect dns.GRAPHQL carried — and is now on the configured hostname. Fn::GetAtt RepositoryUri on AWS::ECR::Repository rebuilt {account}.dkr.ecr.{region}.amazonaws.com/{name} instead of reading the repositoryUri ECR had just returned, so a stack published a registry no docker push in this environment can reach while aws ecr describe-repositories on the same repository returned the working one; neither dkr nor ecr is a host-route label, so the stack-output re-hosting could not correct it after the fact. Unchanged and deliberate: Lambda FunctionUrl, API Gateway v2 apiEndpoint, AppSync dns.* and CloudFront DomainName were already host-routed; SQS queue URLs and Cognito's OIDC issuer stay path-style because AWS addresses those as service endpoints too. See docs/plans/host-routing-precedence.md §8.

  • CloudFront (ViewerProtocolPolicy) — a distribution set to redirect-to-https no longer redirects to a scheme this server does not answer. Overcast listens for TLS only when OVERCAST_TLS_CERT and OVERCAST_TLS_KEY are both set, but the policy was enforced unconditionally, so on a default (plain HTTP) run every viewer request was answered with 301 https://{host}:4566/… — a URL that gives the browser a TLS handshake against an HTTP listener. redirect-to-https is a common template default, so a distribution could be completely unusable in the browser with nothing in the response explaining why. https-only had the same defect one step earlier, returning a 403 HTTPS is required that no client could ever satisfy, making the distribution permanently unreachable. Both are now enforced only when TLS is actually configured, and otherwise served as allow-all with a one-time warning naming the two environment variables; a TLS-terminating proxy in front that sets X-Forwarded-Proto: https continues to satisfy the policy as before. This is a deliberate divergence from AWS, which always serves HTTPS and can therefore always satisfy both — pointing a caller at an endpoint the emulator cannot answer is the worse of the two, the same trade already recorded for AppSync's uris map and Cognito's OIDC issuer scheme.

  • Networking (host-based addressing) — a Host header is now matched case-insensitively in every part, so the same address in any case reaches the same service. A hostname is case-insensitive (RFC 4343; RFC 3986 §3.2.2 for the URI authority), but only the base domain was folded — the .s3. separator, the bucket name, the service label and the region segment were all compared verbatim, so casing decided which service answered a request. An upper-case service label missed the dispatch table entirely and S3 virtual-hosted addressing then took the whole {id}.{label}.{region} as a bucket name, so UrlId.Lambda-Url.US-East-1.localhost — a Lambda function URL — came back as S3's NoSuchBucket, and the API Gateway and AppSync invoke hosts did the same. This is not a hand-typed edge case: a browser lower-cases the Host before sending it, so it is what a user gets by pasting an address Overcast minted into the address bar. CloudFront is where it surfaced, because its distribution IDs are the only upper-case ones Overcast mints (E + 13 upper-case alphanumerics) and the lookup was verbatim: pasting a minted DomainName answered 502 Bad GatewayDistribution "e…" not found — while the identical request with curl -H Host: succeeded. Host-derived distribution IDs are re-canonicalised to upper case, the only form the store holds. An upper-case region segment was worse than a misroute, because that region becomes the state store's key prefix: a request on …execute-api.US-East-1.… partitioned its resources into a region no other request could name. S3 virtual-hosted addressing is fixed in both forms (MyBucket.localhost and mybucket.S3.localhost now resolve, where a bucket name is lower-case-only by AWS rule so an upper-case Host segment could only ever have meant a case-folded name). The same rule now applies on the way out, so what Overcast mints is canonical rather than an echo of however the caller typed the Host: every client-facing URL funnels through one helper (serviceutil.RequestBaseURL), so a queue URL, invoke endpoint or stack output created over MixedCase.localhost:4566 came back carrying that casing and was not string-equal to the same resource's URL fetched over the lowercase host, despite addressing one endpoint. requestContext.domainName and domainPrefix — data function code can branch on — and CloudFront's redirect-to-https Location are folded for the same reason. One shared implementation now serves both directions, since two copies of a case rule is exactly how this arose. Deliberately not folded: the Host used to verify a SigV4 signature, which must stay byte-identical to what the client signed. Paths are untouched and stay case-sensitive, matching AWS. Classification remains allocation-free on every path — folding is pay-per-use, so a host that is already lower-case is returned unchanged — at a measured cost of 4–13 ns per request, well inside the existing budget; see docs/plans/host-routing-precedence.md §4 and §7. Separately, a host-routed invoke now resolves in the region its own URL names: the region hint was read off the Host by a second, older service list that had drifted from the routing table and knew nothing of appsync-api, appsync-realtime-api or lambda-url, so an AppSync API or Lambda function URL created outside the default region was invoked against the default region's partition of a region-scoped store and answered NotFoundException: GraphQL API … not found / 404 Function not found. These are exactly the requests with no SigV4 to fall back on — AppSync host-routed invokes authenticate with an x-api-key header and a Lambda function URL with nothing at all — so the Host was the only region evidence they carried. Lambda's case survived a comment asserting the lookup was region-agnostic, which held only for resolving the URL config: the getFunction call immediately after it is region-scoped. Region extraction now reads the same label table the router dispatches on, so registering a host-routed service can no longer leave it resolving in the wrong region, and the documented precedence is unchanged — an explicit X-Overcast-Region, then the SigV4 credential scope, then the Host — so a signed request's scope is never overridden by its Host. The same read also stopped taking the base hostname for a region when the address has none: an unsigned request to an ordinary S3 virtual-hosted address such as mybucket.s3.localhost.overcast.sh resolved to the region localhost and partitioned that request's state under a name nothing else could reach — the same failure as an unfolded region segment, and invisible to signed callers because the credential scope resolves ahead of the Host.

  • Per-service storage overridesOVERCAST_STATE_<SERVICE> naming a service that does not exist is now a startup error instead of being silently ignored. The override was read by probing OVERCAST_STATE_<name> for each known service, so a variable that loop never thought to construct — OVERCAST_STATE_CLOUDWATCH_LOGS for a service actually named logs, any typo, or a name that predated a rename — did nothing at all: no error, no warning, and the service quietly kept using the global backend while its owner believed otherwise.

  • SQS (dead-letter queues) / Web UIListDeadLetterSourceQueues returned its source-queue list under QueueUrls, but AWS names that one member lower-camel queueUrls — the single SQS list response that does, where ListQueues and the rest capitalise it. AWS SDKs decode an absent member as an empty list rather than erroring, so every SDK client, not just the console, saw a dead-letter queue with no source queues and had no way to distinguish that from a queue nothing dead-letters to. The Query-protocol XML was never affected and still renders repeated <QueueUrl> elements, because AWS models the member with that xmlName. The web UI gates DLQ redrive on this call exactly as AWS does — StartMessageMoveTask accepts "only ARNs of dead-letter queues (DLQs) whose sources are other Amazon SQS queues" — so the queue detail page's redrive button had never appeared for any queue. It now does, and is no longer additionally disabled while the DLQ happens to be empty, a restriction AWS does not impose. Individual messages on a DLQ also gained a button that returns just that one message to the queue it failed on, routed by its DeadLetterSourceQueueArn the same way a whole-queue redrive routes each message back to its own source.

  • Lambda (Runtime API) — an invocation is no longer stranded when a function has more than one execution environment. GET /2018-06-01/runtime/invocation/next is a long poll, and the server tracked one waiting container per function ARN, so the second container to poll replaced the first one's channel. The displaced container stayed blocked on a channel nothing would ever send to and never re-checked the queue, while SubmitInvocation delivered into the surviving buffered channel — a send that succeeds even when the reader has gone. Either way the invocation was never handed to a running container: the function logged START, produced no output for its entire configured timeout, and was reported as a timeout that the handler had no part in. The invocation also stayed in the function's queue after the caller gave up, because CancelInvocation only removed it from the pending map, so the next container to start picked up work that had already been reported as failed and ran the handler for real — side effects included — under a dead request ID, its response then discarded as "invocation not found". A second, later invocation would find the queue still holding the stale one and take that instead, so once a function tipped into this state it stayed there. Waiters are now tracked per container in FIFO order, an invocation claimed by a poll that unwinds is redelivered rather than lost, cancelled invocations are dropped from the queue and skipped at pickup, and a container's parked polls are released when it is unregistered. The Extensions API had the same hazard on /2020-01-01/extension/event/next — an event handed to an unwinding poll vanished, and a lost SHUTDOWN is an extension never told to flush — and is now requeued the same way. Functions with a layered extension hit this most often (the AWS Parameters and Secrets Lambda Extension being the common case) because a longer, more variable cold start widens the window in which two containers poll at once.

  • Web UI (event stream) — the Events page now opens with the history that preceded it instead of starting from whenever you first looked at it. The server has always replayed its rolling history buffer to every new SSE client, and the app shell has always opened that connection on load, but the captured events were being thrown away before they could be shown. The subscription writes into the React Query cache with setQueryData, and nothing observes that query unless a consumer is mounted — so away from the Events page it was an unobserved query, garbage collected after the default five minutes, and setQueryData does not reschedule that timer. Opening the Events page afterwards then created the query afresh and ran its queryFn, which resolves to an empty list, wiping whatever had accumulated since the last collection. Both stream queries are now pinned for the lifetime of the page, registered as query defaults so the retention holds from the first event written rather than from whenever something first subscribed. Retention was also being undone from the other end: the SharedWorker kept the last 1,000 events and each tab the last 5,000, both as flat FIFOs, so the UI's own polling — every request publishes a request:Received — filled them with telemetry and pushed out exactly the state changes a developer was looking for. Every client-side buffer now holds 10,000 events, matching the server's own capacity, and evicts the way the server does: oldest request telemetry and heartbeats first, oldest overall only once no noise is left. Backscroll is also the first thing given back under memory pressure — the working capacity starts lower on a low-RAM device and halves while the JS heap is near its limit, recovering as room returns. Events are now delivered from the worker in batches rather than one message per frame, which matters most on the path that was already correct: a fresh load replaying thousands of events cost a postMessage, a cache write, a re-sort of the whole buffer and a full query-invalidation pass each, with the event→query-key map rebuilt every time. Ordering is fixed too — events were sorted by comparing timestamps as strings, and Go's RFC3339Nano drops trailing zeros, so an event at …:05.5Z sorted before one at …:05Z. Finally, /_events now gives every frame an SSE id and honours the Last-Event-ID a reconnecting client sends back (or a last_event_id query parameter, for the reconnects a browser does not manage for us, where a fresh EventSource has no memory of the id — the BFF forwards both). Without it every reconnect replayed the whole buffer and a client that had kept its own copy ended up holding several: a laptop waking from sleep was enough. The id names the run as well as the position, so a token minted before a restart is discarded rather than believed — sequence numbers restart with the process, and honouring a stale one would silently skip the beginning of the new run. The resume point is the highest sequence number below which everything has been sent, not the newest id written, because the bus dispatches through a worker pool and events reach one subscriber out of order; that costs a handful of duplicate events at a drop and can never lose one.

  • Web UI (navigation under load) — clicking to another service now responds even while the emulator is busy. Routes are code-split, so a first visit to a page fetches its JavaScript chunk from the same HTTP/1.1 origin that carries the app-wide SSE stream, Lambda invoke progress streams, S3 transfers and polling queries — and a browser allows six connections per origin, so under a Lambda burst the chunk fetch could not start and the click looked dropped, with nothing painted to say otherwise. Every route's chunk is now warmed through the router's own chunk loader while the browser is idle after first paint — code only, one chunk at a time; loaders and queries still run on intent and navigation — so navigating stops needing the network once the tab has had a quiet moment. When a navigation does still wait (the first seconds of a session, or a loader in flight), the content area now paints the design system's static skeleton rows almost immediately (after 50 ms, held ≥ 300 ms once shown) instead of nothing. The burst itself is tamed at both ends: the event stream's flush window now widens 50→400 ms under sustained pressure and narrows back when traffic subsides, on the same halve/double hysteresis the buffer already uses for heap pressure, and each query key is invalidated at most once per second with a trailing call — so a burst no longer refetches instance lists and the topology graph twenty times a second, returning those sockets and that main-thread time to the page, while sparse events stay exactly as fresh as before.

  • Web UI (pinned sidebar items) — dragging a pinned service to reorder it no longer navigates to that service, and no longer scrolls the sidebar sideways. Two separate defects, both in the drag interaction. First, dropping a row — inside the sidebar or anywhere else on the page — followed the link under the pointer as a full page load. Once a drag activates, dnd-kit adds a capture-phase click listener on the document that calls only stopPropagation(); that is enough to stop React's synthetic onClick, which is exactly where TanStack Router's <Link> would have called preventDefault() and routed client-side, but the anchor's default action was never cancelled, so the browser navigated itself. Because the sortable translates the row rather than rendering a drag overlay, the link under the release point was often a neighbouring pinned service, so the reload frequently went somewhere the user had not dragged from. A guard now cancels the click that terminates a drag; it is armed from onDragStart, which fires only once the sensor's 6px activation distance is met, so a click without a drag is untouched, and a fresh pointerdown disarms it so a drag that never produced a click (Escape, released off-window) cannot swallow an unrelated click later. Second, the dragged row was given a horizontal translate that chased the pointer even though the list only reorders vertically. That pushed it past the nav's right edge, which grew the nav's scrollable width, which stopped dnd-kit's auto-scroller seeing the nav as fully scrolled and set it scrolling sideways — and that scroll was fed back into the drag transform, pushing the row further right again. Dragging right and holding took scrollLeft past 15,000px. overflow-x: hidden does not prevent this, as the element is still a scroll container with no visible scrollbar. The drag is now pinned to the vertical axis, which removes the feedback loop at its source rather than damping the auto-scroller.

  • Lambda (invoke) / web console — a function that runs longer than 30 seconds now completes when invoked from the console's Test tab. The BFF proxied the invoke progress stream through the shared HTTP client, whose 30-second Timeout covers reading the response body, so any longer invocation had its upstream request cancelled mid-run: the browser saw the stream close with no result and reported "Stream ended without result", and the emulator recorded the cancellation as if the function had overrun — logging lambda invoke timed out and writing Status: timeout into the CloudWatch REPORT line for a function whose configured timeout, up to AWS's 15-minute maximum, had not been reached. That pointed straight at the handler for a fault that was in the proxy, and was most visible on functions doing real work during INIT — fetching secrets through the AWS Parameters and Secrets Lambda extension, for instance — where the wall clock includes cold start. The stream is now proxied with the streaming client, and emits SSE keepalive frames every 15 seconds while the handler runs so a long invocation does not sit on an idle connection that proxies and browsers drop. A caller disconnecting is now reported as a cancellation with Status: error, distinct from a timeout; a function that genuinely overruns returns AWS's payload — <timestamp> <request id> Task timed out after N.NN seconds — rather than a Go error string wrapped in Runtime.ExitError. Container teardown after either outcome stops the container with a 5-second grace period, so the docker:ContainerDied event that follows reports exit code 137 whenever the runtime does not handle SIGTERM; that is the teardown, not the cause.

  • Networking (container DNS) / Lambda / ECS — containers Overcast starts can now reach it by every hostname it advertises, not just the exact ones. The split-horizon names were pinned with Docker's --add-host, and /etc/hosts is an exact-match table with no wildcard syntax, so a subdomain — {bucket}.s3.localhost.overcast.sh, {apiId}.execute-api.{region}.localhost.overcast.sh, the forms AWS SDKs actually build — missed the table, fell through to public DNS, and resolved to 127.0.0.1, which inside a container is the container itself. Because the lookup succeeded, it surfaced as a refused connection rather than an unknown host, and virtual-hosted S3 addressing and API Gateway invoke URLs were unusable from function code even though Overcast has always routed those Hosts server-side. Enumerating entries could not fix it: bucket names and API IDs are minted after a container starts, and a container's /etc/hosts is fixed at creation. Overcast now serves the zone itself (internal/dns, UDP and TCP) and points containers at it with HostConfig.Dns; Docker keeps its embedded resolver in front and uses Overcast's as an upstream, so container-name service discovery and external names are unaffected. Answers are per caller, because Overcast attaches to overcast_lambda and overcast_ecs by default and has an address on each. AAAA for an owned name is answered NODATA rather than forwarded, since the public record is ::1 and a dual-stack client would take it and dial its own loopback; forwarding is refused for peers outside loopback and RFC1918, so a published port 53 cannot become an open resolver. Disable with OVERCAST_DNS=false; a failed bind is logged rather than fatal, as port 53 needs privilege outside a container, and either way the /etc/hosts entries still cover the exact hostnames.

  • Lambda / ECS (endpoint)AWS_ENDPOINT_URL inside containers is now a hostname (http://localhost.overcast.sh:4566) rather than Overcast's raw address. An IP endpoint forces every AWS SDK to path-style addressing and makes the virtual-hosted URLs an SDK derives from the endpoint unusable, and an address baked into a warm execution environment stops working if Overcast's container is recreated on a different one. /etc/hosts and the resolver still target the address, so the endpoint resolves whether or not the resolver is running. Invoke payloads now get the same URL rewriting function environment already receives: a queue URL minted by a host-side caller and handed to Invoke was the last route by which an unreachable origin reached a container, because AWS SDKs resolve the SQS endpoint from the QueueUrl rather than from AWS_ENDPOINT_URL.

  • CloudFormation (intrinsic functions)Fn::Join and Fn::Select now resolve a list argument that is itself an intrinsic, and Fn::FindInMap, Fn::Base64 and Fn::Cidr are implemented. Fn::Select/Fn::Join asserted their second argument was already a list, so Fn::Select [n, {"Fn::Split": [...]}] resolved to an empty string and Fn::Join formatted the unresolved Go map into its output verbatim. That broke every CDK stack containing a nested stack: CDK builds the nested template's S3 key as Fn::Select [n, Fn::Split ["||", ...]], so the key came out empty, the TemplateURL collapsed to /<bucket>/, S3 answered that as a ListObjectsV2 with HTTP 200, and the stack failed three layers away with cannot unmarshal string into cloudformation.Template. The same defect broke Fn::Select [0, {"Fn::GetAZs": ""}], the standard idiom for placing a subnet in an availability zone. Fn::FindInMap was never implemented at all — Mappings was parsed and carried on the resolve context but never read — so region-keyed lookups resolved to the raw map; a miss now yields an empty string rather than embedding Go's map formatting into a resource property (real CloudFormation rejects the template, which the resolver has no way to signal). Fn::Base64 was recognised by the YAML short-form parser but had no resolver. Fn::Cidr is new, IPv4 only. Fn::ToJsonString and Fn::Length remain unimplemented; both require the AWS::LanguageExtensions transform. With these, the CDK compat suite goes from 4 passed/1 failed/30 skipped to 35 passed, 0 failed across the full bootstrap → synth → deploy → verify → update → destroy lifecycle.

  • CloudFormation (AWS::Events::EventBus)Ref now returns the event bus name, as AWS documents, rather than its ARN. Consumers build an ARN as arn:aws:events:<region>:<account>:event-bus/ + name, so a Ref passed onward produced a doubled ARN (…:event-bus/arn:aws:events:…:event-bus/my-bus). Fn::GetAtt Arn still returns the ARN. This was invisible until the nested-stack fix above let a CDK deploy get far enough to exercise it.

  • Router (unimplemented AWS operations) — 675 more modeled AWS REST operations now return a protocol-correct 501 NotImplemented instead of an S3 error. A REST path shared by several AWS services — GET /tags/{resourceArn} is bound by roughly seventy of them, DELETE /workspaces/{workspaceId} by a handful — was treated as unclaimable and handed to S3's catch-all, so an SDK calling one got NoSuchBucket rather than an honest "not emulated". The ambiguity only ever decided which service to name, never whether S3 owned the path: the fallback already requires a SigV4 credential scope that is present and not S3's, and no AWS SDK signs an S3 request as another service. That scope is therefore sufficient evidence on its own, and an exact modeled method-and-path match now returns the modeled 501 whether or not one service can be singled out. Requests that are unsigned, or signed for S3 or an S3-family service (s3-object-lambda, s3-outposts, s3express, which speak the S3 API under their own signing names and were previously only partly recognised), still reach S3 unchanged. Two further APIs that the credential scope alone cannot place are now resolved from the evidence they do carry: S3 Control shares S3's signing name but sends x-amz-account-id on every operation, which S3 never does; CodeCatalyst models no SigV4 name at all and authenticates with a bearer token, an auth scheme S3 has no mode for. Both are only consulted when the scope is otherwise indistinguishable from S3's, so S3 traffic pays one header read and never reaches the operation trie. No modeled non-S3 operation now falls through to S3, and a router-level corpus test over all 18,720 of them keeps it that way.

  • CloudFormation (stack outputs) — a stack output naming one of Overcast's own path-style URLs is now re-minted on the origin the caller reached Overcast on, so it is dialable by whoever asked for it. Resource handlers build these URLs while provisioning, from the configured origin — provisioning runs through internal requests, so there is no caller to derive one from — and that value was then handed to every later caller unchanged. With the API port remapped (docker run -p 4600:4566, as scripts/run-test-instance.sh does) a stack's queue-URL output named port 4566 while the same server's GetQueueUrl returned 4600, and the output was undialable: AWS SDKs resolve the SQS endpoint from the QueueUrl rather than from client configuration, so a JS SDK client reading the stack output failed with the queue URL's origin substituted for its endpoint. The AWS CLI masked this by honouring AWS_ENDPOINT_URL. Host-routed outputs were already corrected this way; this extends the same treatment to path-style URLs whose origin is recognisably Overcast's own, leaving ARNs, ECR URIs and third-party endpoints untouched.

  • CloudFormation — stack outputs that name a host-routed AWS endpoint are now returned on an origin the caller can reach. CDK composes an API Gateway invoke URL in the template itself, with the scheme as a literal and no port (["https://", {"Ref":"Api"}, ".execute-api.us-east-1.", {"Ref":"AWS::URLSuffix"}, ...]), so DescribeStacks handed back https://abc.execute-api.us-east-1.amazonaws.com/prod/ — a URL that resolves to real AWS. AWS::URLSuffix cannot express a scheme or a port, so it still resolves to amazonaws.com as it does on AWS; the correction happens at output emission, where the finished string is parsed with the same grammar that routes inbound requests and re-minted through the same helper every service uses. Outputs that are not host-routed AWS endpoints — ECR registry URIs, S3 URLs, ARNs, plain strings — are left untouched, because Overcast does not serve those hostnames and must not claim to.

  • Networking (host-based addressing) — three more AWS hostname forms now reach the right service, and the recognised-domain list is no longer duplicated. {apiId}.appsync-realtime-api.{region}.{host} was not registered, so it was claimed as an S3 bucket named {apiId}.appsync-realtime-api.{region} — a subscription to the hostname AWS actually serves, and the one Amplify derives by substituting into the GraphQL URL, landed on the S3 handler. It now routes to the same endpoint as appsync-api, with the query string preserved because AppSync carries connection auth there. {distributionId}.cloudfront.net now reaches the distribution proxy instead of falling through, and Distribution.DomainName is minted on the hostname the caller reached Overcast on ({id}.cloudfront.{host}) rather than the literal cloudfront.net, which is a fixed AWS domain Overcast cannot serve without a DNS override. And localhost.floci.io was missing from the S3 virtual-hosted base list while internal/containerendpoint already advertised it, so a bucket was unreachable on a domain Overcast tells users resolves to it; both now derive from one list in internal/config, with a test that fails if they diverge again. docs/networking.md gains an inventory of every known AWS resource subdomain and whether Overcast routes it — AWS publishes no such list, and the SDK endpoint rulesets and Smithy endpointPrefix cover control-plane endpoints only.

  • API Gateway (HTTP v2) / AppSync — the endpoint URLs these services report are now ones Overcast can actually serve. API Gateway v2 never populated apiEndpoint at all, so CloudFormation's Fn::GetAtt ApiEndpoint resolved to an empty string; it now returns the canonical {apiId}.execute-api.{region}.{host} endpoint on the hostname the caller reached Overcast on, and is omitted when DisableExecuteApiEndpoint is set, as AWS does. AppSync's dns map hardcoded amazonaws.com, advertising names that resolve to real AWS and can never reach the emulator; it now carries the configured hostname. dns.REALTIME deliberately reports the same host as dns.GRAPHQL, because Overcast colocates the GraphQL and realtime endpoints and appsync-api is the host that routes — an appsync-realtime-api name would be one Overcast hands out but cannot serve. AppSync's uris map deliberately stays path-style: the host-routed form needs *.localhost to resolve, which it does not on Windows by default, and advertising a URL the caller may be unable to dial is worse in practice than the shape difference. All three services now mint through one shared helper, so a URL Overcast returns is by construction one its own router resolves back to that resource.

  • S3 (bucket naming) — bucket-name validation now matches AWS's documented rules in both directions. Names AWS accepts were being rejected: any name containing a period (example.com, www.example.com and my.example.s3.bucket are AWS's own documented valid examples — periods are discouraged, because they break the *.s3.<region>.amazonaws.com wildcard certificate for virtual-hosted-style addressing over HTTPS, but they are legal), and any name with consecutive hyphens, which AWS permits — it forbids two adjacent periods, and its own reserved suffixes --ol-s3, --x-s3 and --table-s3 contain double hyphens. Rejecting a valid name is the worse direction, since it fails a stack locally that deploys fine against AWS. Names AWS rejects were being accepted: the reserved prefixes xn--, sthree- and amzn-s3-demo-, the reserved suffixes -s3alias, --ol-s3, .mrap, --x-s3 and --table-s3, and names containing two adjacent periods. Because periods are now accepted, a bucket whose name carries a host-routed service label (execute-api, lambda-url, appsync-api) as a second-or-later dot segment is reachable path-style and as {bucket}.s3.{host} but not in the bare {bucket}.{host} form, where it parses as a service address; CreateBucket logs a warning naming the label and both alternatives, and still creates the bucket, as AWS does.

  • Networking (host-based addressing) — every Host-routed invoke URL on a hostname Overcast can actually resolve now reaches its service. API Gateway (REST v1 and HTTP v2), Lambda function URLs and AppSync GraphQL were all reachable only on an .amazonaws.com Host, which does not resolve to the emulator without a hosts-file entry; on localhost, localhost.overcast.sh, localhost.localstack.cloud or a configured OVERCAST_HOSTNAME they returned 403 Missing Authentication Token. S3 virtual-hosted addressing and host-routed service dispatch were separate middlewares that both claimed the same request: the S3 matcher took everything in front of the base as a bucket name, so abc123.execute-api.us-east-1.localhost yielded a bucket called abc123.execute-api.us-east-1 which was prepended to the path before the service rewrite prepended its own prefix on top, and API Gateway then read the mangled first segment as a stage name. Lambda was the sharpest case, since CreateFunctionUrlConfig minted exactly such a URL and Overcast then refused to serve it. The two are now one decision with a fixed precedence derived from the host grammar rather than from middleware registration order, so a bucket and a service can never both claim a Host. Classification also became allocation-free and runs once instead of twice: a plain path-style request costs 31.9 ns and no heap allocations, down from 230 ns and four (Go 1.24.13, linux/amd64, AMD Ryzen 9 5900X, median of 3).

  • Cognito (OIDC issuer) — the iss claim in every minted JWT, and every endpoint in the OIDC discovery document, now come from the configured external origin instead of the Host header the authenticating caller happened to send. OVERCAST_HOSTNAME was ignored outright, so a token minted for a host CLI claimed an issuer a sibling container could not dial, and vice versa — and because an OIDC client validates iss and fetches signing keys from {iss}/.well-known/jwks.json, the mismatch failed token validation rather than degrading. The scheme was hardcoded http as well, so with OVERCAST_TLS_CERT/OVERCAST_TLS_KEY set the advertised issuer and jwks_uri pointed at a scheme the server does not answer. Both now resolve through the same helper every other client-facing URL uses, which derives host, port and scheme together from config; authorization_endpoint, token_endpoint, userinfo_endpoint and revocation_endpoint moved with them. Token validation is unaffected — the pool ID is read from the issuer's trailing path segment, never compared against a recomputed string — so tokens issued before this change still verify. Separately, a client speaking Smithy RPC v2 CBOR was given a different issuer again: CBOR dispatches to Cognito's typed operation table, whose handlers receive a context rather than a request, and the issuer built there was a bare {region}/{poolId} with no scheme or host at all — unusable for OIDC key discovery and rejected outright by an API Gateway JWT authorizer, which compares the claim exactly. All twelve typed auth flows now resolve the same origin the JSON path does, so a caller's wire protocol no longer changes the issuer their token carries.

  • Lambda / API Gateway / CloudFront (account IDs) — resources and invoke events now report the configured OVERCAST_ACCOUNT_ID rather than the default account. requestContext.accountId was the literal 000000000000 in Lambda function-URL events and in all three API Gateway proxy-event shapes (REST v1, HTTP v2, and v2 routes on the v1 payload format), so customer handler code reading it — to compose an ARN, or to assert which account it is running in — saw the wrong account on any server started with a different one. CloudFront function and realtime-log-config ARNs were hardcoded the same way, which meant a CloudFront function and a CloudFront distribution created on the same server reported different accounts, since distribution ARNs already read config correctly.

  • SMTP (mail capture) — the mock SMTP server no longer hangs on shutdown when a connection arrives while it is stopping. The accept loop's break on a cancelled context ended its enclosing select rather than the loop, so the server closed the connection and then started a session handler for it anyway. That handler released a concurrency slot it had never acquired: with no other sessions in flight the release blocked forever, Serve never returned, and shutdown stalled until the caller's timeout fired; with sessions in flight it took a live session's slot instead, permanently lowering how many connections the server would go on to accept. Reaching it needed a connection accepted in the window between the context being cancelled and the listener closing, and that was roughly a coin flip per connection rather than a rare race, since Go chooses at random when both select cases are ready.

  • Lambda (event source mappings) — shutting the service down no longer hangs waiting for ESM delivery goroutines that have no stop signal to observe. StopAll cancelled only the per-mapping delivery contexts it had registered, but the delivery manager's base context was context.Background(), so the ReloadAll goroutine that resumes persisted mappings at startup — tracked by the very WaitGroup the shutdown drain waits on — had nothing that could cancel it; parked waiting for a persistent store to become ready, it held the drain open indefinitely. A ReloadAll that instead woke up mid-teardown could register a fresh SQS poller after the drain had begun, stranding a poller nothing would stop and adding to a WaitGroup already being waited on. Both windows only opened while ReloadAll was still running at shutdown, so this surfaced as a rare, load-dependent stall rather than a reliable failure. The base context is now cancelled by StopAll, delivery started after shutdown begins is refused, and the drain is bounded by the caller's shutdown context — logging loudly instead of blocking forever, the way cmd/overcast's bounded store close already did.

  • Router / REST APIs — modeled REST JSON and REST XML operations that no configured handler claims now return protocol-correct 501 NotImplemented instead of falling through to S3. The generated path trie is consulted only after explicit routes, and SigV4's modeled service scope disambiguates AWS SDK/CLI/CDK traffic from legitimate S3 bucket and object paths.

  • Router / AWS JSON API — modeled AWS JSON targets that no configured service dispatcher claims now return 501 NotImplemented with the standard unsupported-operation marker and request ID, rather than the legacy 400 UnknownOperationException; existing service dispatchers and legitimate S3 traffic retain precedence.

  • Router / Smithy RPC v2 — modeled RPC v2 CBOR operations, including services that advertise CBOR alongside AWS JSON, now return a CBOR 501 NotImplemented when no configured dispatcher implements them instead of 415 UnsupportedProtocol; a known disabled service returns 503 ServiceDisabled before modeled-operation ownership is considered; RPC v2 JSON uses the same generated ownership path when AWS publishes modeled operations for it, while headerless S3 multipart requests whose bucket/key happens to match the RPC path grammar remain with S3.

  • CloudFormation — a stack can now contain more than one resource of a type that its template does not name, and a failed update no longer destroys the resource it was replacing. Physical names for unnamed resources were built from the stack name alone ({StackName}-Function), so every unnamed resource of a type in a stack got the same name and the second collided with the first — ResourceConflictException: Function already exist. Since CDK almost never names its resources, leaving CloudFormation to generate one, a stack with two Lambda functions could not deploy at all; the same fallback affected queues, tables, topics, buckets, roles, policies, instance profiles, secrets, log groups, log streams, SSM parameters, layer versions and event buses. Generated names now follow CloudFormation's own shape, {StackName}-{LogicalID}-{RANDOM}, truncated to each service's limit with the unique suffix preserved. Separately, replacing a resource deleted the old one before creating its replacement, so if the update then failed anywhere the original was already gone and rollback had nothing to restore — the update destroyed the resource outright. Replacement now creates first and defers deleting the original to a cleanup phase that runs only once the whole update succeeds, so a failure rolls back to the intact original and removes the replacement instead; this is what the random name component is for, since the two must coexist. An unnamed resource also keeps its generated name across an update rather than being renamed into a needless replacement, and the removal phase is now reported as AWS reports it — UPDATE_COMPLETE_CLEANUP_IN_PROGRESS and UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS on the way to the matching terminal status, so a stack held up by a resource that will not delete is visibly in cleanup rather than looking mid-update. Two further rollback defects went with it: a rollback rebuilt the stack's resource list from a map the update had been emptying as it went, so every resource handled before the failure was dropped and the stack came back believing it owned nothing while those resources still existed — the next update would then try to create them on top of themselves; and a replacement under UpdateReplacePolicy: Retain left its replacement behind on rollback, so the retained original and the abandoned replacement both survived with nothing recording which one the stack owned.

  • Web UI (container port mapping) — the console now works when Overcast's ports are remapped, e.g. docker run -p 4580:4566 -p 4581:4567. The SPA is handed its API endpoint by the server, which built it from the port Overcast listens on; inside a container that is the internal port, so a console opened on the published UI port was told the API was on localhost:4566, which is closed on the host, and every request from it failed. Overcast now asks Docker for its own container's port bindings and gives the browser the published host port, falling back to the listen port when there is no answer to be had — not containerised, no Docker socket mounted, or the port not published — so native binaries and 1:1 mappings are unchanged. The /api/* proxy the console calls server-side is unaffected by the same change: an endpoint naming Overcast's own published port on a loopback host is mapped back to the internal port before being dialled, since that port exists only outside the container. This affected every non-default port mapping, including the port pair scripts/run-test-instance.sh picks.

  • Router / Query API — unclaimed AWS Query GET and POST actions now return
    501 NotImplemented with the standard unsupported-operation marker and
    request ID, rather than GET falling through to S3 or POST returning a bare
    400; requests for a disabled Query service now report ServiceDisabled.

  • ECS — task containers can now actually reach Overcast. AWS_ENDPOINT_URL was hardcoded to host.docker.internal, a name Docker Desktop synthesises but native Linux does not resolve at all, so on Linux hosts every task's AWS calls failed outright; it now carries a real address — Overcast's own IP on the ECS network when it runs in a container, otherwise the host address on the interface carrying the default route (skipping link-local 169.254.0.0/16 addresses and virtual switches a container cannot route to), with host.docker.internal kept only as a last resort and paired with a host-gateway /etc/hosts entry so it resolves off Docker Desktop too. Separately, AWS SDKs resolve the SQS endpoint from the QueueUrl rather than from AWS_ENDPOINT_URL, so a queue URL baked into a task definition by a host-side cdk deploy sent the task's SQS client to its own loopback; loopback origins on Overcast's port are now rewritten in task-definition and RunTask-override environment values, and split-horizon hostnames (localhost.overcast.sh, localhost.localstack.cloud, localhost.floci.io, plus OVERCAST_HOSTNAME and the new comma-separated OVERCAST_SPLIT_HORIZON_HOSTS) are mapped to Overcast in each task container's /etc/hosts, so one URL works from both the host and inside the task.

  • Web UI — expanded sidebar rows are clickable across their whole height and width. The row element carried the padding and the hover highlight while the link inside it only filled the content box, leaving a 10px horizontal / 7px vertical ring that highlighted on hover but swallowed the click; every row in the expanded sidebar was affected — nav links, pinned favourites, and the expand/collapse toggle on services with sub-pages — and the drag grip on a pinned favourite no longer blocks clicks on the row's left edge. Row dimensions and styling are unchanged. An object's name in the S3 bucket listing is now a link that opens the same object inspector the row's eye action does, instead of being inert text. Object rows also no longer show a pointer cursor or the interactive hover tint: those advertised a whole-row click that did nothing but toggle a selection highlight nothing else read. Folder rows, which do navigate on click, keep both. Tables and scrollable panels now share one subtle scrollbar treatment across Firefox, Chromium, and Safari, with the system's own scrollbars preserved in forced-colors mode. On the Metrics & Health page, the Advisories and Storage Activity sections no longer drop back to their loading skeletons every three seconds while debug mode is off, shifting the page below them on every poll: GET /_debug/metrics returning "debug disabled" is now an answer the poll carries rather than a fetch error, which stops the query from being rewound to its loading state on each refresh. The health strip's Journal Mode also no longer reports "Debug mode required" when debug mode is on and the backend is memory-only — there is no SQLite journal mode to report there, and it now says so. On the Lambda function detail page, the Test, Versions, Monitor, Configuration and Triggers tabs no longer open flush against the tab bar: each panel rendered at a 0px offset, so bare headings and text sat directly on the rule and a narrower card read as jammed under it. They now use the same 16px offset the ECS, Cognito and VPC detail pages already did. The Code tab keeps its flush alignment, since its editor is a full-width bordered slab whose top edge continues the rule. Every copy-to-clipboard control in the console now works outside a secure context: navigator.clipboard is gated on one, so it is absent — not failing, absent — whenever the console is served over plain HTTP from anything other than localhost, and all thirteen copy buttons read it directly. On http://localhost.overcast.sh, the hostname Overcast now points containers and docs at, on a LAN address, or on a container hostname, clicking Copy did nothing at all and said nothing about it. Copies now fall back to a hidden textarea driven by document.execCommand("copy"), which has no secure-context requirement, and both outcomes are reported as toasts rather than only the happy one — several controls (EC2, ECS and RDS detail pages, the system map) previously gave no feedback either way, so a silent no-op was indistinguishable from success. Copy is now one component across the console, which also gave the icon-only buttons the accessible names they never had, and added a copy action to each row of the event stream that yields the whole event envelope as formatted JSON. Reading the clipboard has no such fallback — execCommand("paste") is refused everywhere and Firefox never exposes readText to page scripts — so Lambda's "Paste .env" control is now disabled with an explanation where it cannot work, instead of appearing live and doing nothing.

  • CloudFormation / Lambda — functions declared with inline code (Code.ZipFile) are now packaged into a real deployment archive, so they can actually be invoked. A template's Code.ZipFile is source text while the Lambda API's is a base64 zip; the source was passed straight through, so every inline-code function created cleanly and then failed at invoke with build code tar: open zip: zip: not a valid zip file. Affects AWS::Lambda::Function on both stack create and update, and therefore any CDK construct that injects an inline handler — including BucketNotificationsHandler, which CDK adds automatically to any stack whose bucket has notifications, so stacks that declared no Lambda of their own were affected too. The console also no longer presents its example "Hello from Lambda!" stub as a function's real code: unreadable packages are labelled as examples, and code stored unpackaged by earlier versions is now shown as-is. Existing broken functions are repaired by redeploying the stack.

  • Lambda — changing a function's configuration now retires its warm execution environments immediately instead of leaving the old containers serving invocations until the 15-minute idle timeout. Previously only UpdateFunctionCode invalidated the warm container, and only lazily — on the next invocation — so editing environment variables (in the web UI, via the AWS CLI/SDK, or through CloudFormation/CDK) appeared to have no effect: the next invoke was served by the container started with the old values. Now any update that changes what the container was built with — environment variables, memory, timeout, handler, runtime, layers, VPC config, image config, architecture, or code — destroys idle containers as soon as the update is stored, and lets containers that are mid-invocation finish before destroying them, matching AWS, which never interrupts an in-flight invocation. Updates that change nothing the container can observe, such as the description or role, keep the warm containers, so cosmetic edits cost no cold start. The same retirement applies to code refreshed reactively from S3. GET /_lambda/instances and the system map now report one entry per execution environment. They were keyed by function name, so a function running five concurrent invocations reported a single instance; the web UI had been built for multiple all along but the data could never contain them. Two related leaks are fixed alongside: deleting a function stopped its containers but left phantom instances listed for 15 minutes, and a retired environment stayed listed as reusable until the idle sweeper ran. a warm container that disappears without Overcast asking — removed with docker rm -f, OOM-killed, or lost to a Docker restart — is now dropped from the warm set as soon as the Docker event stream reports it. Previously the pool kept treating it as warm, so the next invocation was handed the dead container, submitted the event to a Runtime API nothing was polling, and hung for the function's entire configured timeout before failing (only the invocation after that cold started). Docker container-died events are now also subscribed reliably: the subscription raced Docker probing, and if probing won, exit detection was silently disabled for the process lifetime, meaning a container that crashed mid-invocation also hung until the timeout instead of failing immediately.

  • SQS, Lambda — queue URLs are now minted per request, on the origin the caller reached Overcast on, so a host CLI and a sibling Lambda container each get a URL they can actually dial. This matters because AWS SDKs resolve the SQS endpoint from the QueueUrl rather than from client configuration and AWS_ENDPOINT_URL does not override that (see the ECS entry above for the mechanism), so a single server-wide hostname could only ever satisfy one side: an SQSClient inside a Lambda function was sent to its own container by a localhost:4566 queue URL, while the documented Docker Compose setup handed host-side clients an overcast:4566 URL that does not resolve on the host. OVERCAST_HOSTNAME becomes the fallback for callers without a usable origin rather than a server-wide override, requests arriving on a real AWS hostname still fall back to it, and queue URLs minted on any origin remain accepted since only the queue name is read from them. Lambda containers additionally get the same environment-rewriting and split-horizon /etc/hosts treatment as ECS tasks, so a queue URL baked into function environment by a host-side cdk deploy reaches Overcast too. That rewriting now also recognises URLs minted against the host port Overcast is published on, not only the port it listens on: a container started with remapped ports (docker run -p 4580:4566, as scripts/run-test-instance.sh does) listens on 4566 while host callers mint queue URLs on 4580, so a baked-in URL was previously left untouched and the function's SQS client dialled its own loopback on a dead port. URLs a function fetches itself were never affected.

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