v0.0.1-alpha.36
Pre-releaseOvercast v0.0.1-alpha.36
Docker Images
Full image with the web management console:
- Pull:
ghcr.io/neaox/overcast:0.0.1-alpha.36 - 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.36 - 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.36
docker run --rm -p 4566:4566 ghcr.io/neaox/overcast-slim:0.0.1-alpha.36Native Binaries
Download a binary for your platform from the assets below and verify it with SHA256SUMS.
| Asset | SHA256 |
|---|---|
overcast-darwin-amd64 |
f1e331f6b72d556bfff0d13a597ac4d5a3622144ae29edb29ed3a71d5e186e04 |
overcast-darwin-arm64 |
960186b1e74347ac3fdf84446224c576dc10ac2c4f96ece987f58d5366c73d33 |
overcast-linux-amd64 |
c4568d5ceeba438cf02381dcf5846dcf7fce293f9ba40407bee2d0012d51e15c |
overcast-linux-arm64 |
75ff98f34ebea38e5f9686b01166543057862c48238ad9def000571b0de820f3 |
overcast-windows-amd64.exe |
4e1ee01ea6e3815e08db2fc31f4eeacdf52802c435fcf79f42f45f9a816fcd25 |
overcastd-darwin-amd64 |
8c77d83924aa0d29f5e22c677b783c3585b3a8034daeff8081db3640720a802a |
overcastd-darwin-arm64 |
258a71aecbbc9e3d804cd55e559b3b9fea2a45794ffa9ec56b3ce6535c6acdc7 |
overcastd-linux-amd64 |
08e3f7564f4388fafc149e446e51bfefe2c5f927271f32224b2c97a5300cc177 |
overcastd-linux-arm64 |
daeab55224f40c0d612a832e2df00f77c7f78d1a2994b5c8d9e2a39d6f2ad6ee |
overcastd-windows-amd64.exe |
ca41a8c095c2feb04879e89343e9d3b514614cf6442e6022d79a6fd0ddb1ba50 |
Release Notes
Added
-
[mcp] a request that asks for a streamed response now gets one: the notifications belonging to that request arrive as they happen, followed by its result. Progress, log messages and cancellation are scoped to a single request — MCP revision
2026-07-28says such notifications "flow only on the response stream of the request they relate to" — so a long tool call reports its progress on its own response instead of on a separate connection, and a client reading in order sees the work reported before the answer to it. Cancelling a call deliversnotifications/cancelledon the cancelled call's stream, which then ends without a result. A request that did not ask for a stream is answered exactly as before, and a request refused before it reaches a handler is still an HTTP error rather than a stream carrying one -
[web/tls] a Settings page in the console, whose first section drives HTTPS setup end to end: it reports the serving mode, certificate and trust-store state, mints the CA and server certificate on a click, and installs the CA into the system trust store behind the OS approval prompt. A containerized daemon still prepares the certificate material, so the CA is downloadable at
/api/ca.pembefore the restart, and hands back the host-sideovercast https enable --endpoint ...one-liner. The one step no UI can take — restarting withOVERCAST_TLS=auto— is shown verbatim in per-shell tabs, and a "Switch to HTTPS" button appears once the TLS listener answers -
[web] a header gear opening that Settings page, in every build including bundled ones; the connection is editable there as well as from the existing header plug, both sharing the first-run dialog's form and its live reachability probe
-
[web] an empty CloudFormation, SQS or Lambda list page in the console now says when the resources exist in a different region — "No stacks in
us-east-1. There are 3 inap-southeast-2." — with a button that switches to it. A developer whoseAWS_REGIONpoints somewhere the console does not sees a working emulator and an empty list, and nothing on that screen is about regions, so it reads as a lost deploy or a broken emulator. The notice appears only when the selected region is genuinely empty and another genuinely is not: an account with nothing in it anywhere says nothing at all -
[tls]
OVERCAST_CA_DIRputs the local CA somewhere other than$OVERCAST_DATA_DIR/ca, so a container can mint certificates from a CA that outlives it. State and a trust anchor have opposite lifetimes — state is disposable, while installing a root certificate is a per-machine, permanent act — and tying them to one directory is what made everydocker compose downcost another root-certificate approval. Create the CA once on the host withovercast https enable, mount it read-only (-e OVERCAST_CA_DIR=/ca -v ~/.overcast/data/ca:/ca:ro), and recreation,down -vand image upgrades all keep working with no further prompts; this is the anchor-durable/leaf-ephemeral split mkcert, Caddy and step-ca use. Read-only mounts are supported directly: leaf caching becomes best-effort (a re-mint costs about a millisecond) rather than a startup failure, and because a bind-mounted CA carries the host's ownership and0600keys that the unprivileged container user usually cannot read, the entrypoint serves from a private in-container copy when that happens — the mount is never written to.docker-compose.ymlnow gives the CA its own volume -
[events/web] every event on the stream now carries the request id of the API call that caused it, so a
PutObjectand the notification, queue write and delivery it set off are all attributable to one call instead of appearing as unrelated activity; the bus derives it from the context a publisher already passes, which covers asynchronous work — an SNS fan-out delivering after itsPublishreturned is attributed to thatPublish— and leaves it off entirely for events no request caused, such as container and timer activity, rather than inventing an id that resolves to nothing; the Events page shows the id in the expanded event and in what the copy button writes, matches it in the filter box so pasting one narrows the console to that call's events, and links each row to the request's trace whenOVERCAST_DEBUGis set; a trace's own Events tab correspondingly lists everything its request caused, where it previously showed only the single row summarising the request itself
Changed
-
BREAKING [mcp] the log level is stated per request instead of set once for a connection, and a request that names none is sent no
notifications/messageat all.logging/setLevelset a threshold that every client on the server then shared, which MCP revision2026-07-28replaces with a level named in each request's_meta— so one client can ask for debug without making another listen to it. Silence is the default rather than a threshold nobody chose: the revision requires that a server not emit log notifications for a request that did not ask for them. Deciding this before the notification is built also means an emit with no audience costs nothing, which matters because these fire from the emulator's own paths whether or not MCP is in use
migration: putio.modelcontextprotocol/logLevelin a request's_metato receivenotifications/messagefor it; requests that omit it are answered without any -
[mcp] a client is told the tool, prompt or resource list changed without having to complete a handshake first. Emission used to be gated on the
initializelifecycle, which MCP revision2026-07-28removes: a stateless client never performs one, so a provider registering after startup would have gone unannounced to it. A client that opened asubscriptions/listenstream and named the notification type has already said it is ready to receive, which is what the gate existed to establish -
[web] the Metrics & Health page is reordered — health and runtime facts in one summary band, then a uniform runtime card grid, then advisories and per-subsystem diagnostics; the advisories section collapses to a single line when there is nothing to report, and the Docker table moved to the foot of the page and now shows each service its own socket
-
BREAKING [ec2] every EC2 describe now refuses a filter name it does not implement, with AWS's
InvalidParameterValue: The filter '<name>' is invalid. It used to answer one of two ways, and which one depended on which helper the handler happened to be written with: theparseFilterValuesdescribes (VPCs, subnets, security groups, route tables, internet gateways, VPC endpoints, peering connections) looked each filter up by name, never saw the rest, and returned everything; thematchFiltersones (NAT gateways, network interfaces) compared every supplied filter against an attribute map and returned nothing. Both are confidently wrong, in opposite directions, in one service, so a caller could not even work around them consistently. This is the root of thetag:Namebug fixed in #1033 — a find-or-create script'sdescribe-vpcs --filters Name=tag:Name,...was ignored, soVpcs[0]was the seeded default VPC and the script adopted it instead of creating its own private one; implementing that one filter fixed that one filter, while the next unimplemented name failed in exactly the same silent way. Which names each operation implements is now declared in one place per operation, and that declaration is what matches the filter, writes the error and is checked against the capability tables — which the same change makes accurate, several having claimed filters that were never applied. The set is also wider than it was:DescribeAddresses,DescribeImages,DescribeKeyPairs,DescribeRegions,DescribeAvailabilityZones,DescribeInstanceTypesandDescribeTagsimplement filters they previously accepted and ignored, andDescribeInstancesgainedinstance-id, which its capability note had claimed for some time. CDK's VPC context provider sends only names Overcast implements, on all four describes it calls, soVpc.fromLookupandcdk deployare unaffected
migration: a call passing a filter Overcast does not implement now fails instead of returning a wrong answer quietly. The error names every filter that operation does support, so the fix is usually to drop or rename the filter;docs/services/ec2.mdlists the same sets. This is about filter names only: filter values gained AWS's*and?wildcards later in this same release, so aMachineImage.lookupwhosenamefilter is a wildcard resolves rather than returning nothing — see the entry below -
BREAKING [ec2] a filter name is matched exactly, as AWS matches it.
Name=VPC-IDandName=isdefaultare refused with theInvalidParameterValueany unrecognised name gets, where the pre-#1038parseFilterValuesdescribes folded case and accepted them. The other two filter idioms it replaced were already case-sensitive, so this settles a split rather than tightening a uniform behaviour, and it is what real EC2 does
migration: spell the filter name the way the AWS docs spell it — lower-case and hyphenated for all butisDefault, and the error message names every spelling the operation accepts -
BREAKING [autoscaling]
DescribeTagsrefuses a filter name it does not implement with AWS'sValidationError, and implements all four names AWS documents —auto-scaling-group,key,valueandpropagate-at-launch. Onlyauto-scaling-groupwas read before and the rest were ignored, so a filter onkeyorvaluereturned every tag in the account. It also read only the first value ofauto-scaling-group, so a caller asking about three groups was answered about one of them and told nothing about the other two
migration: a call passing a filter name outside those four now fails instead of returning everything -
BREAKING [ssm]
DescribeParametersrefuses aParameterFilterskey or option it does not implement, with theInvalidFilterKeyandInvalidFilterOptionerrors AWS declares on the operation. It applied a filter only when the caller had spelledKey=Name, Option=BeginsWithexactly, and ignored everything else — so a filter onType,KeyId,Path,LabelorTier, orOption=Equals/ContainsonName, answered with every parameter in the account presented as a filtered result. The same eight lines were written twice, in the legacy handler and the typed path, so the two could also disagree; there is now one declaration behind both.Typeis implemented alongsideName, andContainsalongsideBeginsWith
migration: a call passing a filter key or option Overcast does not implement now fails instead of silently returning everything; the error names what is accepted -
[tagging] tag output is key-sorted everywhere rather than in Go map order. Tags are held in a map and Go randomises map iteration, so 19 render sites handed a client a different order on every call — churn for anything diffing two responses. Rendering now goes through one shared helper that sorts, so a service cannot render an unordered map by accident; this also replaces the eight hand-rolled sorts that each solved it locally.
DescribeTagsis ordered by resource ID and then by key -
BREAKING [tagging] tag keys and values are checked against the character set AWS documents for them,
^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$— letters, digits, spaces, and_ . : / = + - @— which nothing enforced anywhere. It applies to every service that shares the validator. Keys AWS itself stamps are unaffected:aws:is reserved from callers, not from AWS, so theaws:autoscaling:groupNamethat Auto Scaling puts on the instances it launches is still accepted
migration: a tag carrying a character outside that set —!,%,(, a tab or a newline are the common ones — is now refused by every service that validates, where it was previously stored. Rewrite the tag, or drop the offending character. Nothing already stored is re-validated or removed
Fixed
-
[backup]
GetBackupPlanis reachable from an AWS SDK. AWS binds it toGET /backup/plans/{backupPlanId}/with a trailing slash — unlikeUpdateBackupPlanandDeleteBackupPlanon the same resource, which carry none — and only the slash-less spelling was registered, so a signed client got a 501 and an unsigned one fell through to S3's wildcard and got aNoSuchKey. Both spellings are now registered, as they already were for the three collection operations -
[cloudformation/ecs/web] failed ECS service deployments preserve the reason-bearing scheduler failure, create rollback honours
RetainandRetainExceptOnCreatedeletion policies, and the stack Events tab automatically loads older paginated history from its nested scroll region -
[cloudformation] no-op stack updates no longer retrieve dynamic secret references for unchanged resources, matching AWS's intentionally stale resource behavior
-
[cloudformation] stacks no longer complete around resources that are still being created: ElastiCache caches, MSK and EKS clusters, EFS file systems, mount targets and access points, and Lambda functions are now held open until the service reports them ready, and a resource that reaches a failure status fails the stack with that status rather than a timeout. A stack whose ElastiCache or MSK resource cannot get a container therefore fails on the reason the moment it happens, rather than after the full stabilization budget — fifteen minutes for a cache and forty-five for a cluster, spent waiting on a record nothing was ever going to move. Those two waits now run in every deployment: they were skipped where no Docker socket was configured for the service, because a resource stuck in
creatingwould have held the stack open for the whole budget and then rolled it back, and with both services now settling a metadata-only resource immediately there is nothing left to skip -
[cloudformation] a failed deploy's evidence is captured before rollback deletes it and served from
GET /_overcast/cloudformation/stacks/{stackName}/diagnostics— the stopped ECS task's exit code, the container's own output, and the ECS scheduler's events, each tagged with whether real AWS would have given you the same thing; the AWS API surface is unchanged, and a successful deploy clears the entry -
[docker/ecs/ecr/efs/eks/elasticache/msk/rds] Docker-backed services reconcile one shared daemon snapshot at startup and after event-stream reconnects, immediately replace failed managed runtimes, and avoid retaining stale container state; ECS also preserves a missed container's exit code and Docker stop time before replacing the task with current launch-time secrets
-
BREAKING [ec2] tag filters work, and a resource's tags are one thing rather than two.
DescribeVpcsdid not implementtag:<key>and ignored it silently, sodescribe-vpcs --filters Name=tag:Name,Values=...returned every VPC in the region — a find-or-create script readVpcs[0], got the seeded default VPC, and adopted a public VPC it never meant to use.tag:<key>,tag-keyandtag-valueare now implemented on every describe that returns a taggable resource — VPCs, subnets, instances, NAT gateways, VPN gateways, security groups, route tables, internet gateways and network interfaces, the last four of which returned notagSetat all — and an unmatched filter returns nothing rather than everything. Underneath, create-timeTagSpecification.Ntags were written to the resource's own record whileCreateTagswrote to the tag store, and no describe read both: tagging an existing NAT gateway succeeded and stayed invisible, and an instance's create-time tags never reachedDescribeTags. There is now one tag store behind both.DescribeVpcsalso returns the tags a caller set — it previously replaced them with Overcast's ownovercast:network-statusdiagnostic, which is now merged in alongside. Tag rendering order and the lifetime of a tag are no longer EC2's own business either — see the[tagging]entries below, which took both repo-wide in this same release
migration: two changes can move data you already have. A tag filter that was previously ignored now selects, so a find-or-create script that had been silently taking its "found" branch will start creating the resource it was written to create — which is the fix, but it will run. And create-time tags on instances, NAT gateways and VPN gateways that were written to the resource record by an older Overcast are not carried into the tag store, so re-apply them withcreate-tagsif you are reusing a persisted state file; state created from this release on is unaffected -
[ec2] a filter value is matched as the pattern AWS treats it as, so
*stands for any run of characters,?for exactly one, and a backslash asks for a literal one. Overcast compared values exactly, which quietly narrowed every wildcard filter to nothing:--filters 'Name=tag:Name,Values=overcast-*'selected no VPC, and CDK'sMachineImage.lookup— whosenamefilter is a wildcard in every documented example — found no AMI and failed the synth withAmiNotFound. Tag selectors get the same treatment, sincetag:Nameis where a wildcard is most often reached for -
BREAKING [ec2] tag keys and values are validated. EC2 stored tags without ever reaching for the shared validator, so it accepted a 300-character key, an empty key, and the reserved
aws:prefix, all of which AWS refuses. Both wire paths validate now, and a create call carryingTagSpecification.Nis refused before it makes the resource rather than after
migration: a tag EC2 previously accepted and AWS would not is now refused, so a script or template setting one starts failing where it used to appear to work. Tag keys must be 1–128 characters, values at most 256, and neither may begin withaws:. Nothing already stored is re-validated or removed -
BREAKING [ecr]
DeleteRepositoryrefuses a repository that still holds images unlessforceis set, answeringRepositoryNotEmptyException(HTTP 400) as AWS does. It previously ignoredforceentirely and answered 200, taking every image in the repository with it — the guard is the only signal a caller gets, so a teardown that meant to be stopped was told it had done the right thing instead. An image that arrived bydocker pushcounts too: the registry is swept before the check, the same wayListImagessweeps it
migration: a script that deletes a repository holding images has to pass--force(force: true), which is what the same script already needs against real AWS -
[ecs/cloudformation] forced ECS deployments launch replacement tasks with current Secrets Manager and SSM values, including CDK nonce-driven deployments
-
[ecs] container logs read through the shared Docker de-framer, so a task started with a TTY no longer has the head of its output eaten and the rest cut at an arbitrary offset
-
[ecs] a task has its containers' final output captured before they are removed rather than racing the removal: the container reaper now takes the logs first, whichever teardown scheduled it, so the logs of a deployment that failed survive the rollback that deletes it and a task stopped by hand with
StopTaskcan still be asked why it stopped. The retained copy is bounded to 16 KiB, carries the time it was taken, and is deleted with the task record instead of accumulating for the life of the process -
[ecs/cloudformation] an ECS deployment now reaches its steady state only once its tasks have stayed up, so a CloudFormation stack no longer reports CREATE_COMPLETE around a service whose containers crash-loop; a service that fails to stabilize also reports the failure rather than "deployment in progress"
-
BREAKING [elasticache] a cache that never comes up now says so instead of reporting itself available or sitting in
creatingforever. Two of the three things that can happen to a create reached a wrong answer. A replication group whose primary node never answered was transitioned toavailableonce its retries ran out (#881), soDescribeReplicationGroupsreported a working cache with nothing behind its configuration endpoint — and unlike every other instance of this bug it did not even need a container to have existed, since a group whose start never reached Docker took the same path. A container the Docker daemon refused outright fell between the owners entirely: the start goroutine logged a warning and returned, and the readiness watch that would have owned the record from there is only scheduled once the start succeeds, so nothing was left that would ever move it. Both now settle a replication group and a serverless cache increate-failedand a cache cluster inincompatible-network, the terminal statuses AWS documents for each of those three shapes — ElastiCache does not give them the same vocabulary, andCacheClusterStatushas nocreate-failedat all — with the engine's silence or the daemon's own error recorded on the record as the reason why. Both the form-encoded and the typed create paths are fixed, so the status no longer depends on which wire protocol the caller's SDK negotiated.incompatible-networkis one of the failure acceptors in theCacheClusterAvailablewaiter, soaws elasticache wait cache-cluster-availablefails and reports rather than spinning out its attempts
migration: a caller that treated anything other thanavailableas "still creating" and kept polling will now see a terminal status; treatcreate-failedandincompatible-networkas failures, which is what they mean on AWS -
[elasticache] readiness is proven with the engine's own protocol rather than a TCP dial. Docker's published port accepts connections as soon as the port proxy is up, well before the engine inside the container has bound anything, so a cache could be reported available while the first command a client sent went to a socket Redis was not on yet. Redis and Valkey are asked to
PINGand must answer+PONG, and Memcached is asked forversion— the same bytesredis-cliand aversionline put on the wire. A-LOADINGor-BUSYreply counts as not ready rather than as an answer -
[elasticache] a cache with no container behind it now reports
availableinstead ofcreatingforever. Without a Docker socket configured nothing is started, so no readiness check was ever scheduled and nothing existed that could move the record on —aws elasticache wait cache-cluster-availablespun until it gave up over a cache that was already as ready as it would ever be. A cache cluster, a replication group and a serverless cache created with no container runtime now reachavailableas soon as they are recorded, which is what RDS and Lambda have always done for a metadata-only resource. A daemon that appears later still builds a container for the record and runs a realPINGagainst it -
BREAKING [iam] an operation whose URI carries an ARN is authorized rather than passed through unauthorized. Every AWS SDK percent-encodes an ARN into a single path segment, because a Smithy non-greedy label binds one segment, and the action lookup read the decoded path — so the ARN's
%2Fslashes reappeared as extra path segments, no modeled binding matched, andrequestIAMActionnamed nothing. That isIAMEnforce's one deliberately fail-open branch, so withOVERCAST_ENFORCE_IAM=trueevery such call was served with no policy evaluated at all, including one a policy explicitly denied. MSK's per-cluster and per-configuration surface was the visible case; it held for any service that binds an ARN into a URI. This is the fault PR #1000 fixed in the router's 501 fallback, on the second of the two callers that walk the same generated trie
migration: withOVERCAST_ENFORCE_IAM=true(off by default, and nothing changes with it off), a policy that was never consulted for these operations is now consulted — grant the principal the actions it needs on ARN-addressed paths, such askafka:DescribeCluster,kafka:ListNodesandkafka:ListClusterOperationsfor MSK's per-cluster calls -
[mcp] a resource update with nobody subscribed to it no longer builds a notification body before discovering there is no audience. Providers are handed this emitter directly, so it runs on the emulator's own paths on every resource change whether or not MCP is in use
-
[mcp] a client may now send requests that carry their own protocol metadata, with no
initializehandshake first. MCP revision2026-07-28makes the protocol stateless: instead of negotiating once and remembering the answer for the connection, every request declares its protocol version, client identity and client capabilities in_meta. A request that does so is served on its own terms — the lifecycle gate has nothing left to check, because the request supplies what the handshake used to establish. It is now the only way in, since the handshake it replaces is removed elsewhere in this release. Version disagreements are reported the way the revision specifies — an unsupported version returns-32022listing the versions the server does support, and aMCP-Protocol-Versionheader contradicting the body returns-32020— each with the400 Bad Requeststatus the revision pins to it. A modern client also repeats its method inMcp-Method, and the thing it names inMcp-Nameontools/call,resources/readandprompts/get, so that load balancers and gateways can route and inspect a request without parsing it — and both are now checked against the body, because the value of that mirroring depends on the two agreeing: a header saying one thing while the body says another means an intermediary and the server are handling different requests, which is exactly the confusion the rule exists to prevent, so a disagreement is refused with-32020and400 Bad Request. A name that cannot be sent as plain ASCII is decoded from its base64 form before comparison, so a correct request is not read as a mismatch, and the stdio transport is exempt deliberately: header mirroring is a property of the HTTP binding rather than of the protocol, and stdio has no envelope to mirror into and no intermediaries to serve. The rest of the migration, including retiring the handshake, sessions and stream resumability, is tracked indocs/plans/mcp-2026-07-28-migration.md -
[mcp] every result now declares its type, and the five cacheable ones say how long they may be reused. MCP revision
2026-07-28requires aresultTypeon every result —completefor an ordinary answer, with other values reserved for answers that are not final — and requirestools/list,prompts/list,resources/list,resources/readandresources/templates/listto carryttlMsandcacheScope. Both are stamped centrally where results are written rather than at each of the dozens of sites that build one, so a new handler cannot omit them. The lists advertise a one-minute reuse window, short because those answers genuinely change and thelist_changednotifications are the real mechanism for saying so;server/discoveradvertises an hour, because its answer derives from what is compiled into the binary and cannot change under a running server. All are scopedpublic, since none varies by caller or by credential. One result is not stamped: atools/callanswer is a typed structure with no field to put aresultTypein, and the revision defines an absent one ascomplete -
[mcp] the server answers
server/discover, so a client can ask what it speaks before committing to how it talks. MCP revision2026-07-28requires the method and uses it as the era probe: a client that does not yet know whether a server negotiates withinitializeor serves each request statelessly sends this first and reads the answer. It reports the protocol versions the server supports — one,2026-07-28— along with its capabilities and identity, and answering it confers no state, which is the point of the revision. It is an ordinary request for all that: the protocol version and client capabilities are required in_metaon every request and this method is granted no exemption, so a discover carrying neither is malformed and refused-32602with400 Bad Requestlike any other. A client that does not know which version to name declares the one it speaks and reads thesupportedlist off the-32022refusal — the same information this method would have returned. That list, which this method and the workspacerepo_workspace_infotool both read from one constant, no longer names the retired2025-11-25: a server with no handshake, no session and no GET stream was inviting clients into an era it could not serve, and the-32022refusal handed the same misleading list back to retry with -
[mcp] a long-lived MCP stream now says it is alive, and stops holding the emulator open. Nothing flows on a
subscriptions/listenstream between notifications, and a stream that says nothing for minutes at a time is what proxies and client idle timeouts close underneath you — and what stopped the emulator noticing a client had gone, because it never attempted a write to find out; with thepingutility method removed by MCP revision2026-07-28, a keep-alive is the only liveness mechanism the stream has left. One is now sent every 15 seconds while the stream is idle, as an SSE comment line — the shape the specification points at for a long-lived stream, a line beginning with a colon carrying no event data, which clients ignore rather than treating as a message — so nothing is added to the protocol and no JSON-RPC message is invented, and the interval matches the emulator's two other SSE endpoints. Shutting down no longer waits on the client either: the stream used to select on the request context alone, so the only thing that could end it was the client disconnecting and an attached client that had simply gone quiet never did, which meant one of them was enough to holdovercastopen until it was killed, a graceful shutdown waiting for in-flight handlers. It now takes the same pre-shutdown channel the emulator's other long-lived handlers have always taken, and says so before it goes: the stream is closed with the empty result that marks an orderly end, so a client can tell a shutdown from a dropped connection and decide whether to reconnect -
[mcp] a client can open one long-lived stream with
subscriptions/listenand say on it exactly which notifications it wants. MCP revision2026-07-28replaces both the standalone GET stream andresources/subscribewith this: the request carries a filter naming the notification types and the resource URIs of interest, the server acknowledges with the subscription's id before sending anything else, and every notification on the stream carries that id so a client running several can tell them apart. Types the client did not ask for are never sent, and request-scoped notifications — progress and log messages — never appear here, because those belong to the response stream of the request they relate to. The stream sends a periodic keep-alive comment and ends cleanly when the emulator shuts down, so a client can tell an orderly close from a dropped connection. It is the only way to receive these notifications: the GET stream andresources/subscribeit replaces are removed elsewhere in this release -
[mcp] Cancelling an in-flight request over HTTP could panic the connection serving it. The
notifications/cancelledevent was written to the cancelled request's response stream only after the request had been released, so the response could finish — and net/http reclaim the connection — while the write was still on its way. The notification now goes out first, while the response provably cannot end; a request that finishes of its own accord in the same instant has the late write dropped rather than made -
[msk] a cluster whose broker never comes up reaches
FAILEDcarrying why, instead of sitting inCREATINGforever — whether the broker started and never answered, or its container could not be built at all.aws kafka wait cluster-activeused to spin until it gave up with nothing to say, because the only record of what went wrong was a log line no API caller reads; the reason, or the daemon's own error, is now instateInfo, whichDescribeClusterandDescribeClusterV2both return. Readiness is proven with a KafkaApiVersionsrequest rather than a TCP dial, for the same reason as ElastiCache above. A cluster that is alreadyACTIVEwhen a reconcile or Docker-event start fails is left alone -
[msk] a cluster with no broker behind it now reports
ACTIVEinstead ofCREATINGforever, on the same rule and for the same reason — and matching whatCreateClusterV2already did for a serverless cluster, which has no brokers to provision either.aws kafka wait cluster-activeno longer spins out its attempts against a metadata-only cluster -
BREAKING [msk]
CreateCluster,CreateClusterV2andCreateConfigurationrefuse a name already in use in the same region, answeringConflictException(HTTP 409) as AWS does. Each previously minted a second resource beside the first and answered 200, leaving two clusters — or two configurations — sharing one name for every later lookup by name to guess between. v1 and v2 are two bindings onto one cluster-name namespace, so the guard reads the same records whichever one the create arrived through, and it is scoped to the request's region, since the same name in two regions is two resources on AWS
migration: a script that creates the same cluster or configuration name twice has to delete the first or pick another name, which is what the same script already needs against real AWS -
BREAKING [msk]
UpdateClusterConfigurationtakes the configuration reference from the modeledconfigurationInfoobject and answersNotFoundException(HTTP 404) when the ARN it names does not exist. It read a flatconfigurationArnthat no AWS client sends, so the ARN was always empty, the existence check below it never ran, and an update naming a configuration that was never created was answered 200 with the cluster's version spent on it — the caller told a reference had been resolved that nothing had resolved. The stale-currentVersioncheck is unchanged, and both checks still run before the cluster is touched, so a rejected update does not spend the version it was refused on
migration: a caller that hand-rolls this request body has to sendconfigurationInfo: {"arn": …, "revision": …}in place of a flatconfigurationArn, and a body carrying neither is aBadRequestExceptionbecause the model makes the member required; the AWS CLI and every AWS SDK already send exactly that -
[msk]
DescribeClusterandListClustersreport a cluster in the modeledClusterInfoshape: the Kafka version it was created with arrives incurrentBrokerSoftwareInfo.kafkaVersion, the only member AWS binds it to, so a client can confirm what it provisioned. Both used to marshal the stored record straight onto the wire, which spelled the version as a top-level member no generated client reads and put the emulator internal Docker bookkeeping in the response beside it.DescribeClusterV2andListClustersV2already carried it underprovisioned -
BREAKING [msk]
CreateClusterandCreateClusterV2keep the tags in the create request where the tag operations look, soListTagsForResourcereturns them,UntagResourceremoves them, and every describe and list shows them alongside the onesTagResourceadded afterwards. Creation-time tags went onto the cluster record instead, which no tag operation reads: the call was accepted and the cluster reported itself untagged
migration: a cluster created before this upgrade keeps working, but its creation-time tags are not carried over — re-apply them withaws kafka tag-resource -
[msk] cluster and configuration sub-resources answer a protocol-correct 501 rather than a fabricated "not found". The
/v1/clusters/*dispatch took the whole path tail as the cluster ARN, soListNodes,ListClusterOperations,ListScramSecretsandGetClusterPolicywere told the clusterarn:aws:kafka:…:cluster/my-cluster/<uuid>/nodesdoes not exist, andListConfigurationRevisionsthe same on/v1/configurations/*. Where the ARN ends and a sub-resource begins is now read from the pinned model instead of guessed, so both spellings of the ARN — percent-encoded into one segment, and with literal slashes — keep resolving to the right operation -
[observability] log lines, debug traces and operation metrics name an operation whose URI carries an ARN. The same decoded-path lookup left the label empty, so MSK's per-cluster calls went unlabelled and unmetered, as did every other service that puts an ARN in a URI
-
[opensearch]
ListTagsis reachable from an AWS SDK or the AWS CLI. AWS binds it to the collection URI with a trailing slash —GET /2021-01-01/tags/, the only trailing-slash URI in the OpenSearch model — which is what every client sends and what the route did not match, so a signed client got a 501 and an unsigned one fell through to S3's wildcard object route and got a 404NoSuchKeyfor an OpenSearch call. Both spellings are now registered; the slash-less form keeps working for callers already using it -
[rds]
CreateDBParameterGroupaccepts theaurora-mysql8.0family used by Aurora MySQL 3 instances -
[rds] master accounts now receive version-specific AWS bootstrap privileges and restart-safe credentials instead of stock-image database-scoped grants; Aurora members inherit cluster-owned settings
-
[rds] the retry policy, the deadline and the terminal-failure decision behind every readiness poll come from one shared helper, which RDS and EFS now share with ElastiCache and MSK. Their probes are unchanged — the container inspect, connection and
pg_isready/mysqladmin pingexec for a database, the NFSv4 NULL call for an export — and so is what each of them already did when a resource never answered; what they no longer do is each spell out how long to wait and what "gave up" means -
[router] startup defers event queues, MCP metadata, diagnostic histories, login templates, and Lambda validation patterns until first use
-
[router] a modeled operation whose URI carries an ARN now reaches its 501 instead of falling through to S3. Every AWS client percent-encodes an ARN into a single path segment, because a Smithy non-greedy label binds one segment, and the REST fallback classified the decoded path — so the ARN's
%2Fslashes reappeared as extra path segments, no modeled binding matched, and S3's bucket/object wildcard answered.aws kafka list-cluster-operations-v2, correctly signed, came back<Error><Code>NoSuchKey</Code>…v2/clusters/arn:aws:kafka:…/operations</Error>. That is the fault #963 records, reached by a second route, and it held for every service that puts an ARN in the path -
BREAKING [ssm] an omitted
ParameterFiltersOptionmeansEquals, as it does on AWS. It previously meant "apply no filter at all", so the comparison a caller gets by default —{Key: "Name", Values: ["/app/db"]}— matched every parameter rather than the one named
migration: a caller relying on an unfiltered result from a filter with noOptionwill now get the exact match they asked for -
[eventbridge/elasticache/rds/scheduler] tags die with the resource they describe. Tags live in a namespace keyed by resource ID, so nothing tied them to the record's lifetime and four services never removed them:
ListTagsForResourcekept answering for a rule, bus, cache, DB instance, cluster, subnet group, parameter group, schedule or schedule group that had been deleted, and the blob stayed in the store for the rest of the session. Deleting any of them now takes its tags with it. EventBridge also keyed a rule's tags by whichever ARN spelling the caller used, so tagging a default-bus rule throughrule/<name>hid the tags fromrule/default/<name>and from the delete path; both spellings now reach the same tags. EventBridge's two wire paths minted different ARNs for the same rule or bus — one honouring the request's region, one always the configured default — and now agree -
[tls] Settings → HTTPS says when a containerized daemon's CA will die with the container, and shows how to hand it one that will not — the console's own status endpoint reports
caEphemeral, so this is a fact about the running daemon rather than general advice -
[tls] the daemon no longer logs
http: TLS handshake error ...: client sent an HTTP request to an HTTPS serveronce every health-check interval underOVERCAST_TLS=auto. The image's probe tried plain HTTP first and fell back to https, so a perfectly healthy TLS container produced a handshake error every 5 seconds and buried the ones that meant something; the probe now tries https first, which a plain-HTTP daemon rejects client-side without logging anything. The compose files carried an http-only probe that replaced the image's, sodocker compose upwith TLS on marked the container unhealthy for the life of the run — both now mirror the image -
[tls] the API and the web UI no longer share one
*tls.Config.ServeTLSmutates the config it is handed (it appends toNextProtosto advertise HTTP/2), so passing the same pointer to both servers was a data race, reported bygo test -race, on the ALPN list each listener offers; each server now gets its own copy -
[tls]
overcast https enable --endpoint ...is spelledhttps://wherever a TLS daemon is the target — the startup hint a containerized daemon logs, the console's host-side command once TLS is on, the CLI help, and the docs. Thehttp://spelling still works (the fetch retries over TLS), it just made the daemon log a handshake error for a command Overcast itself had handed the user -
[web/tls] Settings → HTTPS now has a final step for the thing that breaks after the switch: repointing SDKs and the CLI at the https endpoint. It gives the exact
AWS_ENDPOINT_URL/--endpoint-urlvalue for this daemon's port, and names the CA bundle forAWS_CA_BUNDLEandNODE_EXTRA_CA_CERTS— which the AWS CLI and Node.js SDK need even after the CA is in the system trust store, since both ship their own root bundles and never consult it. The step is also shown when TLS is already on, which is when people find their CLI broken -
[web] syntax-highlighted log rows render as a single text node where the CSS Custom Highlight API exists (Chrome 105+, Safari 17.2+, Firefox 140+) — token colours paint as static ranges, so there are no per-token spans for extensions and the accessibility tree to walk, large documents tokenize in a persistent worker, and the cached Prism markup renders unchanged everywhere else. The highlight sets themselves are only ever rebuilt and swapped in wholesale, never mutated range-by-range — the one bulk path Firefox's engine affords — so scrolling any distance in either direction, and a busy live tail, perform no per-range highlight work at all: this closes a Firefox lockup a 31-second profile showed spending 15 seconds deleting ranges one at a time, and its sequel where re-adding them cost a 21-second stall on a jump back to the top
-
[web] code comments in syntax-highlighted previews (S3 objects, HTTP bodies, the code browser) now render muted instead of full-strength foreground — the token theme gained a comment colour it never had
-
[web/logs] a missing log group or stream no longer renders as an empty one. Opening a stream URL while the region selector points at a region where the group does not exist showed the ordinary "No log events" empty state — the server's
ResourceNotFoundExceptionwas swallowed, so a wrong-region visit read as an application that never logged. The stream viewer and the log group page now show a not-found state naming the group (or stream) and the region, other load failures surface with the server's message instead of reading as an empty stream, and a failed cross-stream search (an invalid filter pattern, say) says the search failed rather than claiming "No matching events" -
[scheduler]
TagResourceandCreateScheduleGroupreadTagsas the list of{Key, Value}structures AWS models, andListTagsForResourceanswers with the same list, ordered by key. The member was decoded as a map, so every AWS SDK andaws scheduler tag-resourcesent the modeled array and got aSerializationExceptionback — nothing outside the emulator could put a tag on a schedule or schedule group, inlineTagsonCreateScheduleGroupwere dropped without a word, and an untagged resource rendered an empty object where aTagListwas modeled -
BREAKING [scheduler] tag keys and values are validated, through the shared validator EventBridge and the other services on it already use. Scheduler reached its tag store directly and ran none of those checks, so it accepted a reserved
aws:key prefix, keys over 128 characters, values over 256, and characters outside the set AWS documents — tags real AWS refuses, stored here without complaint, so a template that worked against Overcast failed on deploy.TagResourceand the inlineTagsonCreateScheduleGroupnow both runserviceutil.ValidateTags, the validator EventBridge next door has always used. The count limit is applied to the merged result rather than to each request, so fifty tags added one call at a time is still refused at fifty-one, and a create refused for its tags no longer leaves the schedule group behind
migration: a tag Scheduler previously accepted and AWS would not is now refused withValidationException, so a script or template setting one starts failing where it used to appear to work. Tag keys must be 1-128 characters, values at most 256, neither may begin withaws:, and both are limited to letters, digits, spaces and_ . : / = + - @. Nothing already stored is re-validated or removed -
[state] starting a persistent-store daemon while another is opening the same database file no longer fails on the lock.
PRAGMA user_versionis the first statement the migration runner issues, and switching a new database to WAL takes an exclusive lock, so connections racing to initialise one path collided — eleven of twelve, in a reproduction — and the loser died withmigrate: state: read user_version: database is lockedbefore serving anything. That read now waits the lock out, using the same transient-error classifier and retry delay every other read in the store already uses; a lock nobody releases still surfaces rather than hanging, and an error that is not the busy class still fails immediately -
[state] the store's transient-SQLite classifier now recognises extended result codes, which the driver enables on every connection.
database is locked (261)— SQLITE_BUSY_RECOVERY, the busy a reader gets while another connection recovers the WAL of the same fresh file — compared unequal to the primary SQLITE_BUSY constant, so the busy-class retries (including the migration runner's newuser_versionwait) never engaged for it and concurrent fresh opens could still fail on slow disks. The classifier now compares the primary code in the low byte, covering SQLITE_BUSY_RECOVERY, SQLITE_BUSY_SNAPSHOT, and SQLITE_LOCKED_SHAREDCACHE; genuinely fatal codes such asfile is not a databasestill fail fast so an unusable database degrades the store promptly instead of sitting out the retry budget -
[cloudwatch] Live Tail no longer drops events when a producer writes many small PutLogEvents calls — the session buffer is now counted in events (5,000, matching AWS) rather than write batches (10), and an update that follows an overflow reports
sampled: trueinstead of losing lines silently -
[cloudwatch/web] the log views stay responsive at tens of thousands of lines: live tail arrivals batch per frame into a bounded 10,000-event buffer that reports what it drops, the map's log peek is virtualized with stable row identity, syntax highlighting and row metadata are cached instead of recomputed per scroll frame, and the stream viewer pages past FilterLogEvents' 10,000-event cap (marking the count "N+" while more remain) instead of silently truncating; the Lambda monitor tab's long-claimed 5-second auto-refresh now actually happens
-
[cloudwatch/web] log search and history navigation follow the AWS console's patterns. The log group page's cross-stream search results are virtualized and render through the shared log-row pipeline — level tints and badges, ANSI colour, the search's matches marked, the stream column on the right — where they were previously a plain table with none of these; clicking a result opens its stream centred on that exact event, scrolled into view and persistently highlighted, with the preceding fifteen minutes of context above (a copyable per-row link reopens the same view, and the anchor survives a fresh fetch in another tab — identity is the event's timestamp plus a content signature). The stream viewer loads history older than its window on demand: nearing the oldest edge extends the time window backward in doubling chunks — FilterLogEvents pages forward only, so "older" is time-window expansion — prepending with no viewport jump and stopping honestly: "Start of logs" when history is exhausted (bounded by the streams'
firstEventTimestampmetadata, no empty-window probing), "Start of range" when a user-set start time is the floor. A jump-to-timestamp control repositions the view on the first event at or after a chosen time via the same anchor machinery -
[cloudwatch/web] a live tail says what it is doing, and survives the ways a session actually dies. The list's edge states its condition — "End of logs", "Loading more events…", "Live tail — watching for new events", or "Live tail disconnected" — instead of empty space that read the same in every case, and a session-health dot on the Tail button and the map peek's Logs tab pulses green while the StartLiveTail stream is open and turns red, still, when it is gone. A dead session is detected even when nothing errors: a staleness watchdog reads the emulator's once-a-second heartbeat and marks the session dead after ~10 s of silence (machine sleep, NAT timeout — the cases that used to look identical to a quiet stream forever). After a tail death the map's log peek keeps the newest edge reachable by walking GetLogEvents' forward tokens, reconciled against what the dead session already delivered so nothing shows twice. (@tanstack/react-virtual bumped to 3.14.9 for its iOS-aware scroll adjustments around the peek's prepend anchoring)
-
[cloudwatch/web] the log stream viewer's quality-of-life set, mirroring the AWS console where it has the feature and the browser devtools where it doesn't: a UTC/local timestamp toggle; opt-in inter-row time deltas beside the Time column; view preferences that survive revisits (mode, Format, Syntax, Wrap, sort, UTC, Deltas, Collapse — localStorage, falling back to defaults silently on corrupt state); an opt-in Collapse mode rendering every row as one fixed-height truncated line with per-row click-to-expand, badges, tints and ANSI intact — which also removes the virtualizer's row-measurement work entirely; an Export control downloading the loaded events in display order as CSV (the console's search-results shape — ISO 8601 timestamps, raw message bytes) or JSON, saying so explicitly when more events remain on the server; a hover affordance on any row carrying a Lambda request id (START/END/REPORT lines, JSON
requestIdfields, the Node runtime's tab-separated UUID column) that filters the view to that invocation, as the quoted FilterLogEvents term typing it would run; and keyboard row navigation — arrows or j/k move a visible cursor, Enter expands the focused row in Collapse mode, c copies its message — announced viaaria-activedescendant, never touching keys typed into inputs, and costing no re-render of row content on cursor moves -
[cloudwatch] reading and writing log events now costs what the page costs, not what the history costs, in both storage backends (memory and SQLite). Paging deep into a busy stream — the log peek walking back 200 events at a time, or the viewer expanding its time window into history — used to slow down linearly with how much newer history the page had to skip (a page 400k events deep in a 500k-event stream answered ~84× slower than one near the tail in memory mode, ~166× over SQLite); token pages now resume by index seek and stay flat at any depth. A
FilterLogEventsscoped to one stream reads only that stream's slice of the window instead of the whole group's, andPutLogEventsno longer rescans the stream's persisted history per write batch over SQLite (~129ms per batch at 500k stored events, now sub-millisecond) nor re-sorts the whole stream in memory mode -
[cloudwatch/web] the log viewer's filter placeholder and footer notices no longer render a double-encoded em dash as garbage characters — the strings, along with comments in the Lambda service doc and two test files, had been written back through the wrong encoding; a new CI check (
scripts/check_encoding.py) now fails on mojibake, stray BOMs, and replacement characters so it cannot recur -
[cloudwatch/web] the Format pass is memoised: pretty-printing a log document now happens once per distinct message instead of on every row mount and every Format toggle, so scrolling back over heavy documents and flipping Format stop re-paying multi-millisecond parses per row
-
[cloudwatch/web] scrolling a large log stream no longer locks up or leaves blank regions — rows mounted mid-scroll defer their syntax-highlight markup and hover-action buttons until the scroll settles, cutting the DOM churn that fed browser extensions' form scanners and the accessibility tree (a profile showed 44% of the main thread in those walkers and ~148k node removals in ten seconds)
-
[cloudwatch/web] a single-token log line — a stack dump with no whitespace — no longer forces the whole stream view onto a tens-of-thousands-of-pixels horizontal scrollbar while Wrap is on, leaving the column headers behind: the message block can now shrink to the viewport so break-word has something to break against
-
[s3/web] the bucket object browser scrolls and pages large listings without re-rendering the world: rows are keyed by their S3 identity (prefix, key, key + versionId) instead of their index, so a sort flip or a filter keystroke moves rows rather than remounting and re-measuring every visible one; the next listing page is fetched off the continuation token as the user nears the end of loaded rows, closing a stall where a fast response never re-armed the fetch chain; and visible rows are memoized so scroll frames and unrelated state changes (dialogs, the search box) stop re-rendering them. The object preview dialog now caches its syntax highlighting and declines to pretty-print or highlight documents past the existing 256 KiB formatting cap — saying so in the preview header — instead of freezing at dialog-open on a large JSON or JavaScript object. What is fetched and shown is unchanged: delimiter/prefix grouping, version interleave, scan caps and their "partial" labels all behave as before. The preview also highlights through the same zero-DOM kernel as the log views — one text node with token colours painted as Custom Highlight ranges where the browser supports it, unchanged Prism markup elsewhere
Removed
- BREAKING [mcp] the
2025-11-25half of the MCP surface is gone: theinitializehandshake andnotifications/initialized, protocol sessions and theMcp-Session-Idheader, theDELETEthat ended one, theGET /_overcast/mcpstream and the legacy/_overcast/mcp/ssebridge, stream resumability withLast-Event-IDand theOVERCAST_MCP_REPLAY_LIMITthat bounded it,ping,logging/setLevel, andresources/subscribe/unsubscribe. MCP revision2026-07-28replaces the whole connection model rather than extending it, and Overcast now serves that revision and nothing else:POST /_overcast/mcpis the only endpoint, and every request declares its own protocol version in_metarather than negotiating one. Nothing is lost with the surface:server/discoverreports whatinitializedid,subscriptions/listencarries what the GET stream andresources/subscribecarried, a request's own notifications travel on its response stream, and the keep-alive on a long-lived stream replacespingas the liveness check
migration: point the client at a2026-07-28MCP implementation — a client that opens withinitializehas no fall-forward mechanism, which includes the current TypeScript SDK unlessClientOptions.versionNegotiationis set. RemoveOVERCAST_MCP_REPLAY_LIMITfrom any environment that sets it; it is no longer read
Release: https://github.com/Neaox/overcast/releases/tag/v0.0.1-alpha.36