Custom HTTP handlers - #3880
Custom HTTP handlers#3880
Conversation
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65
✅ Deploy Preview for golemcloud canceled.
|
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Introduce explicit agent kinds, structural file mappings, and the Any HTTP matcher across shared schemas, WIT, protobuf, and SDK metadata emitters. Validate router and filesystem metadata, reject unsupported HTTP deployment compilation, and cover codec, persistence, and extraction boundaries. Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
|
📖 Docs preview: https://docs-ebx2a3y0u-golem-cloud.vercel.app Built from commit |
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Load shared typed-route selection cases and assert fixture-derived captures. Pin root, HEAD, and concrete ANY method distinctions. Test-first commit: typed_literal_dead_end_backtracks_to_parameter_route intentionally fails against the baseline matcher (None instead of complete). The other nine router tests pass. This is a local intermediate commit; the subsequent matcher implementation must resolve the failure before integration. Oracle reviewed; masking failure isolated. Bounded bug-finder run found no bugs in the test changes. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Share strict segment decoding with mapping compilation while retaining declaration-specific restrictions. Preserve original public path, opaque query and trailing-slash intent separately from decoded segments. P01 deliberately rejects malformed escapes, raw non-pchar text, traversal and encoded separators before domain lookup. Root now uses zero segments. Trailing-slash matching is introduced with the compiled match representation separately. Verified common path/mapping/schema tests (10 tests, including 18 path and 11 mapping corpus cases), worker custom API tests (121), and OIDC tests (28). Oracle reviewed; bounded bug-finder reported no bugs. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
…stence Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-30eb-77d5-a492-84dce752c251 Co-authored-by: Amp <amp@ampcode.com>
…ents Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a416-3a9f-758e-9c49-839039333771 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
| /// the returned file has finished streaming. This is observation, not a durable guest invocation. | ||
| /// Metadata is captured from the opened descriptor; path metadata only rejects unsafe opens. | ||
| /// Directories never select an index file. No returned error contains a host path. | ||
| pub(crate) async fn open_file_for_inspection<Adapter: SandboxFilesystemAdapter>( |
There was a problem hiding this comment.
What do we need the new "inspection" operation for, regarding the custom http routing / file serving spec?
There was a problem hiding this comment.
The file operation serves the existing agent filesystem API as well as HTTP live-file delivery; it is not a separate HTTP-only executor API. The old implementation buffered the complete file. Streaming needs safe opening, metadata/byte selection and bounded production while retaining filesystem ownership against concurrent mutation.
Following our discussion, removed the separate inspection scheduling/lifecycle policy. Reads and listings now enter common worker scheduling and use normal activation, initialization, reconstruction, updates and failure handling. Native requests/bytes are not journaled. Existing filesystem ownership is held while the producer reads and released after successful production, rather than waiting for consumer-observed EOF. Static immutable files still use blob storage and need no executor. Runtime tests cover initialization, recovery, Suspend/resume, update and backpressure.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
|
|
||
| /// Executor-local admission, shared by all requests, including requests for absent agents. | ||
| #[derive(Debug)] | ||
| pub struct FileReadAdmission { |
There was a problem hiding this comment.
Do we need a separate mechanism to limit file reads? We already have various limits coming from the user's plan, enforced by the executor.
There was a problem hiding this comment.
Agreed; removed FileReadAdmission, its registry/reservations/semaphore, configuration and admission-only errors. No replacement per-account quota was introduced. Bounded buffering remains per response, but there is no new aggregate outstanding-file-read limit. Slow-consumer backpressure may keep the agent occupied just as ordinary sequential work can; that is the accepted behaviour.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| store.data().set_suspended(); | ||
| /// Publishes the early stream-bearing result and drives its producers to completion. | ||
| /// Live execution and replay use the same trap classification as the guest export. | ||
| pub(crate) async fn materialize_streaming_result<Ctx: WorkerCtx>( |
There was a problem hiding this comment.
Explain what needed to be changed about streaming invocation, why, and how it affects other use cases
There was a problem hiding this comment.
The old suspension point could mark an invocation suspended when the function returned, although returned streams were still running. That left the post-return part of the invocation with incorrect interrupt handling.
We keep execution running through stream materialization/settlement and suspend afterward. This is more than moving one call: interrupt delivery, typed trap/error classification, replay failure handling and session settlement must remain consistent over that extended interval. It affects streaming RPC/entity/custom-HTTP invocations; non-streaming invocations have no additional stream-production interval. Oracle reviewed the broader changes and confirmed they are related correctness fixes, which we agreed to retain with regression coverage.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| @@ -13,22 +13,24 @@ | |||
| // limitations under the License. | |||
There was a problem hiding this comment.
I did not expecct other changes to the invocation loop than supporting the new file-system access commands, explain
There was a problem hiding this comment.
The revision removes inspection-specific scheduling and lifecycle machinery. Native reads/listings use the common loop and normal worker activation/reconstruction/update/failure handling; a suspended unfinished invocation must resume before boundary-only native work runs.
One ordering change remains: main keeps persisted invocations in oplog-derived status and transient commands in an in-memory deque, processing the latter first. To avoid a read overtaking an already accepted invocation, transient filesystem requests capture their position relative to persisted work. select_next_work applies that common policy, giving A -> read -> B when accepted in that order, without journaling the read. invocation_queue contains this ordering/pruning policy, not another execution loop or admission system. The separate streaming-interrupt correctness fix is explained in the adjacent invocation.rs thread. Runtime tests include ordinary Suspend and the concurrent-agent permit-wait path.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| worker_event_service: Arc<dyn WorkerEventService + Send + Sync>, | ||
|
|
||
| queue: Arc<RwLock<VecDeque<QueuedWorkerInvocation>>>, | ||
| queue: Arc<StdMutex<VecDeque<QueuedWorkerInvocation>>>, |
There was a problem hiding this comment.
Restored Tokio's async RwLock. The synchronous lock had been introduced to remove cancelled requests directly from Drop; that mechanism is now removed. Abandoned transient requests are pruned at enqueue/selection and lifecycle boundaries instead. Accepted durable invocations are never pruned just because their caller disconnected. A cancelled transient record may remain until the next pruning boundary, which is the agreed tradeoff.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| // | ||
| // http://license.golem.cloud/LICENSE | ||
|
|
||
| //! Retry-owning invocation sessions used by the custom HTTP adapter. |
There was a problem hiding this comment.
This whole module deserves better structuring and more inline (and doc comment on extracted methods etc) documentation
There was a problem hiding this comment.
Done: split http_session into driver, progress and transport responsibilities and added method/module documentation for protocol state transitions, input/output accounting and recovery ownership. HTTP envelope/body adaptation remains outside the session protocol. The separate recovery correctness changes have regression tests and are described in the RawHandler thread below.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| @@ -0,0 +1,231 @@ | |||
| // Copyright 2024-2026 Golem Cloud | |||
There was a problem hiding this comment.
Follow the existing practice of file layout - if there is a cache/tests.rs then the cache module should be in cache/mod.rs not in cache.rs. Apply this rule to the whole changeset
There was a problem hiding this comment.
Done: applied the directory-module convention throughout the affected modules, including cache/mod.rs alongside cache/tests.rs and the HTTP schema module. Removed the superseded flat module files.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| pub(super) const DOCUMENT_BYTE_LIMIT: usize = 8 * 1024 * 1024; | ||
|
|
||
| #[derive(Clone)] | ||
| pub(super) struct Budget { |
There was a problem hiding this comment.
Removed the OpenAPI-specific Budget, provider/generation/request timeouts, cancellation checks and timeout-driven cleanup. The provider is an ordinary invocation on a fresh ephemeral http-router instance; normal invocation admission/execution owns overload handling. It does not need a separate OpenAPI lifecycle.
Retained document-size limits and a spawn_blocking boundary for synchronous document processing. spawn_blocking avoids running CPU-bound parsing/validation/merge/serialization on an async runtime worker; it is not a timeout or provider-execution mechanism. The remaining bounded_json helpers enforce data-size bounds, not time budgets.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| // Pending entries cannot be evicted by completed-entry LRU pressure. Admission | ||
| // remains with the generation's CPU/cleanup tasks, even after invalidation. | ||
| #[derive(Default)] | ||
| pub(super) struct CacheState { |
There was a problem hiding this comment.
I want you to use our Cache type from golem_common for all cache implementations.
There was a problem hiding this comment.
Done: replaced the bespoke OpenAPI cache with golem_common::Cache, using its spawned singleflight creation path for lazy document generation. Concurrent requests for the same snapshot share generation; cancellation of an individual waiter does not abandon that shared fill. Capacity remains bounded at 256 entries. Removed custom freshness counters, stale-delivery fencing/retry loops and negative caching.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| const GENERATION_CONCURRENCY: usize = 8; | ||
|
|
||
| pub struct OpenApiDocument { | ||
| pub json: Bytes, |
There was a problem hiding this comment.
Did we support JSON before? If not and only yaml, then let's keep it that way
There was a problem hiding this comment.
Confirmed that both JSON and YAML OpenAPI endpoints existed before this PR. Kept both; this is not an added format requirement.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| use std::sync::atomic::{AtomicU64, Ordering}; | ||
|
|
||
| #[derive(Clone)] | ||
| pub(crate) struct Freshness { |
There was a problem hiding this comment.
What's this for? I think the generation and caching of OpenAPI specs got a bit over complicated in this PR. What I think we need is simply a cached openapi document per deployment, lazily generated on first request, and instead of just generating it from the http route metadata as before, it needs to optionally call the component's custom openapi fragment genreator and merge the two. How it calls it is the interesting part and can be completely orthogonal to the cache implementation
There was a problem hiding this comment.
Implemented the simpler model: lazily generate a document from a captured immutable route snapshot, optionally invoke providers through the ordinary ephemeral invocation path, merge their fragments, and cache the result with golem_common::Cache. Provider invocation is independent of the cache implementation.
The cache key is the immutable route-snapshot UUID, so changed route/origin/security inputs use a distinct document identity. Removed OpenAPI-specific freshness generations, stale-delivery fences, retry loops and negative caching. An already-running fill for an older captured snapshot may finish, as agreed; new snapshots use their own keys. Tests cover singleflight generation and snapshot identity/invalidation behaviour.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://license.golem.cloud/LICENSE |
There was a problem hiding this comment.
How is this module related to the http_session module? It's confusing to me
There was a problem hiding this comment.
Separated the responsibilities: MountedDispatch owns mounted-backend selection/dependencies; RawHandler adapts the HTTP request/response envelope and bodies; http_session owns the private streaming-session protocol, transport and recovery. Restructured the session into driver/progress/transport modules with documentation rather than introducing another universal driver.
Also fixed the recovery inconsistencies we discussed, with regression tests: an ambiguous transport failure now retains the exact pending resume attempt ID so an accepted attempt can be replayed. Resume versus Takeover follows attachment state and changes only after an authoritative rejection, rather than unconditionally requesting Takeover or creating a fresh attempt on every retry.
Implemented/reviewed in 71e9f29. Oracle review approved the final fixes. After the latest main merge, CI is green: https://github.com/golemcloud/golem/actions/runs/35634342722 (54 passed, 7 skipped).
Integrate native filesystem work into common resident scheduling and normal lifecycle handling. Remove file-read admission policy and release ownership after bounded production.\n Use ordinary ephemeral OpenAPI invocations and the shared snapshot-keyed cache without provider-specific deadlines or freshness fencing. Restructure HTTP session transport and preserve resume attempts across ambiguous failures. Apply the agreed module, macro and body-schema changes, add recovery regressions, and align executor configuration and guidance. Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Prevent the first round from returning its semaphore permit and letting the next round cancel before the persisted-body gate. Assert that each round starts with its cancellation signal withheld. Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Preserve both HTTP router and local semantic retry documentation in the Rust SDK README. Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
| if !needs_initialization && let Some(invocation) = queue.pop_front() { | ||
| return SelectedWork::Resident(invocation); | ||
| } | ||
| if !status.pending_updates.is_empty() { | ||
| return SelectedWork::ApplyPendingUpdate; | ||
| } | ||
| if let Some(pending) = status.pending_invocations.first() { | ||
| return SelectedWork::Durable(pending.clone()); |
There was a problem hiding this comment.
select_next_workdrains every resident request before checking pending updates or durable invocations. Because filesystem requests enter an unbounded resident deque and this PR intentionally removes aggregate file-read admission, continuous live-file traffic can keep the resident queue non-empty forever, so an already accepted agent method or pending update never runs. This is stronger than merely observing the current state at an invocation boundary. Could we add a bounded fairness rule, plus a test that keeps enqueueing reads while proving a pending invocation and update still make progress?
| let finalized_routes: Vec<_> = finalized_routes.into_iter().map(Arc::new).collect(); | ||
| let openapi_inputs = finalized_routes.iter().find_map(|route| { | ||
| if let RichRouteBehaviour::OpenApiSpec(behavior) = &route.behavior { | ||
| Some(Arc::new(OpenApiInputs { | ||
| key: OpenApiKey::fresh(), | ||
| public_origin: behavior.scheme.origin(domain), | ||
| routes: finalized_routes.clone(), | ||
| })) |
There was a problem hiding this comment.
The review thread describes one lazily generated OpenAPI document per deployment, but
fetch_and_build_domain_apiassigns a fresh random key whenever the domain router is rebuilt. With the default ten-minute router TTL, an unchanged deployment re-runs every provider periodically and leaves the previous document in the 256-entry LRU until eviction. Is that periodic refresh intentional? If not, could the key derive from stable deployment/route identity (including the origin and security inputs) and have a test for a router-cache refill with unchanged routes? If it is intentional, please document the refresh semantics because they differ from the thread resolution.
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub subdomain: Option<DeploymentSubdomain>, | ||
| #[serde(default)] | ||
| pub scheme: golem_common::model::http_api_deployment::HttpApiDeploymentScheme, |
There was a problem hiding this comment.
schemeis needed for a deterministic host-owned OpenAPIserversorigin, but defaulting an omitted value toHttpshere happens before the deployment environment is resolved. Stock local manifests use plain HTTP (*.localhost:9006) and omitscheme, so their generated OpenAPI advertises an unreachablehttps://...server; the human manifest reference does not document the field either. Could the raw value remain optional and be resolved from the built-in target (HTTP for local, HTTPS for cloud), while retaining an explicit override for custom servers? Please also cover an omitted local value and document the field.
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a09fef-5b1e-75be-89e8-6a7c32655f65 Co-authored-by: Amp <amp@ampcode.com>
…corpus Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0d203-35b0-704c-b220-436bda85b67c # Conflicts: # golem-common/src/base_model/diff/mod.rs # golem-common/tests/goldenfiles/diff_model_fingerprint_v12.txt # golem-registry-service/src/api/error.rs # golem-registry-service/src/services/deployment/deployment_context.rs # golem-registry-service/src/services/deployment/write.rs # golem-worker-service/src/gateway_server/tests.rs
…corpus Amp-Thread-ID: https://ampcode.com/threads/T-01a0d203-35b0-704c-b220-436bda85b67c Co-authored-by: Amp <amp@ampcode.com> # Conflicts: # golem-debugging-service/src/debug_context.rs # golem-service-base/src/service/initial_agent_files.rs # golem-worker-executor/src/worker/mod.rs
Purpose
Implements the custom HTTP handler platform (GOL-554–563) and the MoonBit, Rust, TypeScript, and Scala SDK APIs (GOL-564, GOL-565, GOL-567, GOL-568).
defineHttpRouter, Web and raw streaming adapters, host-free contract helpers,exposeFiles, OpenAPI serialization, and CLI integration coverage.The normative
HTTP_HANDLERS.mdspecification is attached to GOL-554, not tracked in Git. The shared corpus lives ingolem-service-base/tests/fixtures/http-handlers/.Routing, authorization, and file serving
Streaming handlers
stream<list<u8>>bodies. Raw adapters preserve valid repeated headers and opaque values, including repeated Set-Cookie; cross-name ordering is not promised.OpenAPI providers
openapi_provider_methodnames an ordinary parameterless, non-streaming method. Providers run only for OpenAPI requests, under a fixed system context and at the selected deployment's component revision; ordinary routing, handlers, and static/live files do not execute provider code.SDKs and remaining scope
MoonBit, Rust, TypeScript, and Scala now expose named ephemeral routers with independently optional handlers, immutable mappings, and OpenAPI providers, plus ordered live-file mappings on eligible ordinary durable agents. They reuse normal configuration, registration, and invocation machinery. Tests cover declaration validation, metadata, generated clients, streaming ownership, and deployed examples. TypeScript provides both normalized Web APIs and a raw escape hatch; Scala does not require an unrelated HTTP/OpenAPI framework.
GOL-566 (Effect HttpRouter/OpenAPI integration) remains follow-up work and is not claimed as implemented here. Low-level Effect metadata support and shared TypeScript contract helpers are present. External deployment/access changes and backward-compatibility layers are not included.
Verification
npm run check:contracts,npm run check:artifacts, and merge diff checks passed.This PR remains unmerged; merging the PR or deploying is not part of this update.