Repository · workstreams
Workstream: HostedJvm
Status: Planned · Component: Maximize developer productivity
This document is an aspirational vision and design-scope document, not a milestone plan. It describes the end-state we are building toward and the architecture decisions that define it; decomposition into concrete tasks happens later. The MVP vs. mature-platform split is the closest thing here to sequencing.
North star
Ask for a JVM — "2 GB, in Frankfurt, Java 21, gone by 18:00" — and get one, already wired for console, health, thread dumps, and heap dumps, that shuts itself down when you forget about it.
Make getting a JVM somewhere a single request. Today, running a JVM that is not on your own machine means choosing a cloud, provisioning a host, installing a runtime, copying a JAR, remembering to tear it all down, and then driving the process with nothing better than SSH. HostedJvm collapses that to request-and-drive: a caller states what the JVM needs (memory, region, Java version, a deadline), the service places it on whichever backend can satisfy the request — a DigitalOcean droplet, an AWS instance, a box the operator already owns — and hands back a handle that is the RemoteJvm control surface: lifecycle, health and memory, console I/O, static-method invocation, jstack- and heap-dump-equivalents, all over url://. Every JVM carries an auto-shutdown deadline, so an abandoned JVM is a bounded cost, never an orphan.
The four verbs in the framing — ask, place, drive, expire — are the four pillars of the design:
- Ask — the request model and the API family: what a caller says, and what comes back.
- Place — environments and placement over pluggable providers that an admin installs and configures, with keys brought by the operator or the user.
- Drive — the control plane the handle exposes — including the utilization history, heap dumps, and thread dumps the service tracks for every lease — and the workloads it can run.
- Expire — leases and the auto-shutdown deadline, the one mechanism the whole service is subordinate to.
Guiding principles
- A JVM is a lease, not a server. Every hosted JVM has a deadline from the moment it is requested. There is no "forever" option; there is only renewal. This is the same stance the DigitalOcean Droplets service takes toward droplets — an orphaned resource bills indefinitely, so termination is the invariant everything else serves.
- The handle is RemoteJvm. HostedJvm does not invent a second way to drive a JVM. The existing RemoteJvmApi contract —
JvmProcesswith health, memory, console,invoke, thread dumps, heap dumps — is what a caller receives, so tooling written against a local child JVM works unchanged against a JVM on the other side of the world. HostedJvm adds where, for whom, and until when; it does not re-describe how. - Providers are plugins; the service is the policy. Which cloud, which account, which region is a provider's business. Who may ask, how much they may hold, how long a lease may run, and which environment is the default is the service's business, held in admin-managed records rather than compiled in. New providers are installed, not deployed.
- Reject what cannot be satisfied; park what cannot be afforded yet. A hard requirement the chosen environment cannot meet (a region it does not offer, more memory than its largest size) is refused with an error that names the requirement and what was available — never silently substituted. A request that is satisfiable but over quota is parked as pending until the caller's quota frees, per Rate Limits — exhaustion parks the request.
- Third parties through their proxies. A provider that reaches a cloud does so through that cloud's
url://3P proxy, never with a raw vendor key of its own. HostedJvm holds proxy credentials; the proxies hold the vendor keys. - Compose with the platform, don't duplicate it. Identity, quota, and billing are shaped for W3Wallet; persistence is the Event Log; workload bytes come from Blobstore, Maven, or a kompile build; droplets come from the droplet proxy. HostedJvm should be remarkable for how little infrastructure it owns.
- Mitigations must be loud. A lease the sweeper terminates, a request parked on quota, a provider that failed to place — each is a first-class recorded event the caller and the operator can see, never a silent outcome.
The request model
A caller asks for a JVM by stating:
| Field | Kind | Meaning |
|---|---|---|
environment |
optional | The admin-defined environment to place in. Absent → the deployment's default environment. |
region |
hard or preferred | Where the JVM should run, in the provider's own region vocabulary (fra1, eu-central-1, local). A hard region is a requirement; a preferred region is a hint the placer honors when it can. |
memoryBytes |
hard | Maximum heap the JVM must be able to run with. Maps to -Xmx plus the host headroom the provider knows it needs. |
cpus |
soft | Desired processor count; the placer picks the smallest size that meets memory and comes closest on CPUs. |
diskBytes |
soft | Scratch disk the workload expects. |
jvmMajorVersion |
hard | 17, 21, … — the provider must have, or be able to install, a matching runtime. |
deadline |
required, defaulted | The instant the JVM is shut down unless renewed. Defaults to the environment's default lease length; capped by the environment's maximum. See §4. |
workload |
required | What to run — see §2. |
labels |
optional | Free-form key/value tags for the caller's own bookkeeping, echoed on every listing. |
What comes back is a lease: a stable id, the resolved placement (provider, environment, region, host, size), the deadline, and a HostedJvm handle that extends JvmProcess. Listing, renewing, and terminating are addressed by lease id — url://hosted-jvm/leases/{id} resolves to the handle, per the resource URL addressing convention.
Current state — built vs. aspirational
The pieces HostedJvm composes already exist; the service that composes them does not.
| Already built | What it provides to this workstream |
|---|---|
| RemoteJvmApi | The per-host contract: RemoteJvmManager builds a JvmProcess (classpath, arguments, environment, JvmSpec selection, heap/metaspace caps, crash heap dumps); the process exposes lifecycle, JvmHealthInfo, JvmMemoryInfo, ordered console output, stdin, invoke, JvmThreadDump, and dumpHeap. It has no notion of tenants, regions, quotas, or deadlines — those are this workstream's additions. |
| RemoteJvmLocal + RemoteJvmAbutment | The child-process backend and the JSON-over-TCP bridge that lets a parent drive it. On a remote host, the agent-side AbutmentServer is the thing a provider stands up next to the user's JVM. |
| RemoteJvmDiscovery | getJvmSpecs() — how a provider learns which runtimes a host has, and therefore whether it can satisfy jvmMajorVersion. |
| JvmLaunchApi | launchJvm(jar) — a local convenience over RemoteJvmLocal. It stays as it is: HostedJvm's local provider uses RemoteJvmLocal directly, and nothing remote goes through launchJvm. |
DigitalOcean Droplets (url://digitalocean-droplets/) |
Droplet creation, expiry via WebCron, renewDroplet, a periodic sweep, and fleet history reconstructed from the vendor's own action log. The DigitalOcean provider is a consumer of this proxy. |
| GithubProxy keys (live page) | The working reference for shared-key and bring-your-own-key registration: an account is registered with a token and a label, the registrant is issued the credential that addresses it, the key is validated before storage and never returned by any read. HostedJvm's provider accounts follow this exactly. |
| W3Wallet | The target shape for identity, allowances, and billing; partially built. HostedJvm is designed to map onto it (§7) without waiting on it. |
Aspirational (the substance of this workstream): everything in the design — the HostedJvm* repository family, the provider SPI and its three initial providers, environments and quota, leases with enforced deadlines, the url://hosted-jvm/ service on ContainerNursery, the CLI, and the WUI.
The design
1. The API family and tenancy
HostedJvm is a new API family rather than an extension of RemoteJvmApi, because the two answer different questions: RemoteJvmApi says how to drive one JVM on one host; HostedJvm says who may have a JVM, where, and until when. Following the standard layered architecture and the FooManager vends Foo tenancy rule:
| Repository | Layer | Contents |
|---|---|---|
| HostedJvmApi | Api | HostedJvmManager (multi-tenant: request, list, renew, terminate, get), HostedJvm (single-tenant handle: extends RemoteJvmApi's JvmProcess with lease metadata — leaseId, environment, placement, deadline), HostedJvmAdmin (providers, environments, accounts — the <Service>Admin interface), and the request/lease/quota data types. |
| HostedJvmProviderApi | Api (SPI) | HostedJvmProvider — the contract a backend implements: capabilities (regions, sizes, runtimes), place(request) → ProviderPlacement, release(placement), reconcile(); plus the account-credential type the provider needs. Kept separate from HostedJvmApi so a provider artifact depends only on the SPI and RemoteJvmApi, never on the caller-facing surface. |
| HostedJvmEmbedded | Embedded | The manager: placement, leases and the sweeper, environments, quota accounting, provider loading, Event Log persistence, reconciliation. Fully testable in-process with the local provider and a ManualClock. |
| HostedJvmServiceServer | ServiceServer | Hosts the Embedded at url://hosted-jvm/; verifies each caller's credential at the boundary (authorization at the url:// boundary). |
| HostedJvmProviderLocal, HostedJvmProviderDigitalOcean, HostedJvmProviderAws | Providers | The three initial backends — see §5. |
| HostedJvmCli | CLI | request, list, get, renew, terminate, console, health, threads, heap, plus an admin namespace. |
| HostedJvmWui | WUI | The operator and user faces — see §11. |
RemoteJvmApi gains only what the hosted case genuinely needs and a local caller also benefits from — chiefly a way to request a runtime by major version rather than by a JvmSpec the caller has already enumerated, and a heap-dump destination that is not a path on the remote host (see open questions). JvmLaunchApi is untouched.
A sketch of the caller-facing contract:
interface HostedJvmManager {
fun request(request: JvmRequest): HostedJvm // may return a lease in PENDING state when parked on quota
fun list(): List<HostedJvm> // the caller's own leases only
fun get(leaseId: String): HostedJvm
fun renew(leaseId: String, newDeadline: Instant): HostedJvm
fun terminate(leaseId: String)
}
interface HostedJvm : JvmProcess { // everything RemoteJvm already offers, plus:
val leaseId: String
val state: LeaseState // PENDING, PLACING, RUNNING, TERMINATING, TERMINATED
val environment: String
val placement: Placement // provider, region, host, size
val deadline: Instant
val terminationReason: TerminationReason? // CALLER, DEADLINE, QUOTA_REVOKED, PROVIDER_LOST, ADMIN
fun utilization(window: Duration): List<UtilizationSample> // heap, non-heap, threads, CPU, GC — sampled by the service
fun captureHeapDump(live: Boolean): HeapDumpCapture // stored in Blobstore, listed on the lease
fun captureThreadDump(): ThreadDumpCapture // stored and listed likewise; also returns JvmThreadDump
fun captures(): List<Capture> // every dump taken on this lease, with who/when/why
}
2. Workloads — what runs in the JVM
A request names a workload from the same sources LambdaServer accepts, so code is met where it already lives:
- A Maven coordinate (resolved from kotlin.directory or Maven Central, with its transitive closure) and a main class or an executable-JAR manifest.
- A JAR in Blobstore, addressed by content hash — the path for anything built locally.
- A kompile build rule from a git repository —
repo+rule, built once by Kotlin Build and the resulting classpath pulled from the shared build cache. - No workload at all — a bare JVM running only the RemoteJvm agent, driven entirely through
invoke,writeStdin, and classpath entries the caller adds later. This is the "give me a JVM to poke at" case and it is first-class.
Every hosted JVM, whatever its workload, is launched with the RemoteJvm agent attached (RemoteJvmAgent on the classpath, AbutmentServer listening), because that is what makes console, health, dumps, and invoke work. A workload that wants to be called by others simply runs a UrlResolver node and announces its own url:// address — the hosted JVM joins the fabric like any other peer, and HostedJvm has no opinion about it beyond having placed it. Exposing raw TCP ports through a provider's firewall is deferred (see open questions).
3. Environments and placement
An environment is the unit an admin configures and a caller names: one provider, one provider account, a set of allowed regions and sizes, a default and maximum lease length, and a per-caller quota (§6). A deployment designates one environment as the default, used when a request names none.
Placement is deterministic and explainable:
- Resolve the environment (named, else default). A named environment the caller is not permitted to use is refused.
- Check hard requirements against the environment's capabilities — region,
jvmMajorVersion, and whether any allowed size meetsmemoryBytes. Any miss is refused with an error naming the requirement and what the environment offers. - Check quota. A request that would exceed the caller's quota in this environment is parked as a
PENDINGlease with a recorded reason and is placed automatically when quota frees (a sibling lease ends). The caller may cancel a pending lease; it never silently expires. - Ask the provider to place: the smallest allowed size satisfying memory, closest on CPUs, in the hard region or the preferred region if offered, else the environment's first allowed region. The provider returns a
ProviderPlacementand aRemoteJvmManagerbound to that host, and the Embedded builds the JVM through it.
In the MVP each lease gets its own host from a cloud provider (one droplet per lease). Packing several leases onto one host is a mature-platform milestone and changes nothing in the API — placement.host simply stops being unique per lease.
4. Leases and the auto-shutdown deadline
The deadline is mandatory and the service's one non-negotiable invariant: no lease exists without one. A request that omits it gets the environment's default (1 hour unless the admin set otherwise); a request that exceeds the environment's maximum is refused, not clamped, so the caller knows the cap exists.
- Renewal —
renew(leaseId, newDeadline)moves the deadline forward (never backward — shortening isterminate), subject to the same maximum measured from now. Renewal is the only way to keep a JVM alive; there is nokeepAliveflag. - Enforcement is provider-independent. The Embedded runs a sweeper on a
Clock(driven byManualClockin tests) that terminates every lease past its deadline:shutdown()the JVM, wait a bounded grace,shutdownNow(), thenreleasethe placement back to the provider. Termination is recorded with reasonDEADLINE. - The provider's own expiry is a second safety net, never the primary. The DigitalOcean provider creates droplets through the droplet proxy, which already attaches a WebCron expiry and a sweep of its own; the provider sets that expiry to the lease deadline plus a short margin, and renews it when the lease renews. If HostedJvm itself is down when a deadline passes, the droplet still dies. A provider that offers no native expiry (local, and AWS until its proxy does) relies on the Embedded sweeper and on reconciliation after a restart (§9).
- Idle shutdown — terminating a lease that has seen no console, RPC, or
invokeactivity for a configurable window — is designed in (it is just anotherTerminationReason) but is not part of the MVP; the deadline alone bounds cost.
5. Providers
A provider is a Maven artifact implementing HostedJvmProvider, installed at runtime by an admin call with its coordinate — the way LambdaServer loads a function from a coordinate — not compiled into the service. Installing resolves the artifact, loads it in an isolated classloader, and registers its capabilities; environments then reference it by name.
The three initial providers, in build order:
- Local (
HostedJvmProviderLocal) — places JVMs on the host the service itself runs on, via RemoteJvmLocal. It needs no account. It exists for self-hosted deployments, for development, and as the provider every Embedded test runs against — it is deliberately not the hosted instance's default environment, because the ContainerNursery host is already shared by many services and is not spare compute. Its capacity is a configured memory budget, not "whatever is free". - DigitalOcean (
HostedJvmProviderDigitalOcean) — consumesurl://digitalocean-droplets/. Its account credential is a droplet-proxy account credential, not a DigitalOcean token: an operator or user registers their DigitalOcean token with the droplet proxy and hands the resulting credential to HostedJvm. Placement creates a droplet (region, size, an image with the requested JDK), waits for SSH, stages the RemoteJvm agent and the workload, and connects anAbutmentClientthrough the fabric. Fleet history and cost come from the proxy's reconstructed record, not from HostedJvm sampling anything. - AWS (
HostedJvmProviderAws) — the same shape over anurl://aws-ec2/3P proxy that does not exist yet. That proxy is a prerequisite of this provider and is sequenced after DigitalOcean and local; the provider is not allowed to hold raw AWS keys to get there sooner.
A provider also implements reconcile(): enumerate what it is actually running under its account and return it, so the Embedded can adopt or terminate hosts it has no lease for.
6. Environments, defaults, and quota
Environments, providers, and accounts are admin-managed records persisted by the Embedded, edited through HostedJvmAdmin (and its CLI and WUI faces), never deploy-time configuration. An environment carries:
- provider name and the provider account it draws on;
- allowed regions and allowed sizes (memory, CPUs, disk) and the JDK versions available;
- default lease length and maximum lease length;
- a per-caller quota: maximum concurrent leases, maximum total
memoryBytesheld, and a rolling allowance of GB-hours per day (memory × time, the unit cost actually scales with); - which callers may use it — everyone, or a listed set — and whether it is the default.
Quota is enforced in the Embedded and accounted per caller per environment from the lease record (every lease knows its memory and its lifetime). Exhaustion parks the request (§3); it does not reject it. An admin may lower a quota below a caller's current holdings, which parks nothing retroactively but blocks new placement and is surfaced to the caller as the reason.
7. Identity, keys, and bring-your-own-key
HostedJvm adopts the registration model GithubProxy runs today, unchanged in mechanism:
- A provider account is registered with
registerAccount(provider, credential, label)and the registrant is issued the credential that addresses it. The provider validates the credential against its backend (for DigitalOcean, that the droplet-proxy credential can list droplets) before it is stored; it is held only inside the service and never returned by any read. - Shared-key mode — the operator registers accounts and binds them to environments open to every caller; callers draw on the operator's budget within quota. The hosted instance's default environment is of this kind.
- Bring-your-own-key mode — a user registers their own provider account and creates (or asks the admin to create) an environment bound to it, visible only to callers the owner lists. Placement in that environment costs the owner, not the operator, and the owner's quota settings are theirs to set.
- Every caller presents its own credential on every call; the service keeps no session. A caller sees only its own leases; an admin sees all.
The whole model is shaped to become W3Wallet-native without an API change: a registered account's capacity is an asset pool (hosted-jvm:gb-hours per environment), a caller's quota is an allowance attenuated from it, and the sweeper's accounting is the metering that settlement will read — see the W3Wallet spend-and-settlement design. Until W3Wallet lands, the Embedded's own quota records play the allowance's part.
8. The control plane over url://
A HostedJvm handle is a JvmProcess, served over url:// by the ServiceServer and backed, provider-side, by the RemoteJvm abutment to the actual process. Everything RemoteJvm offers is available remotely: getHealthInfo, getMemoryInfo, incremental getConsoleOutput(fromIndex), writeStdin, invoke(className, method, args), getThreadDump, and dumpHeap. Beyond passing those through, the service itself observes every lease — a caller should be able to answer "how is my JVM doing, and what was it doing when it went wrong?" without having been watching at the time:
- Utilization is tracked, not just readable. The Embedded samples
JvmHealthInfoandJvmMemoryInfofor every running lease on a fixed cadence (heap used and committed against max, non-heap, per-pool usage, thread count and peak, GC count and time, CPU, loaded classes) and keeps the samples as a per-lease time series — a projection the CLI, the WUI, and other services read viautilization(window). Listings carry the latest sample solistalready shows memory pressure; environments aggregate their leases so an operator sees utilization per environment and per provider account. Samples are retained for the lease's lifetime plus a bounded tail after termination, so the last minutes before a deadline or crash are still there afterwards. - Heap dumps and thread dumps are first-class captures.
captureHeapDump(live)andcaptureThreadDump()take the dump through RemoteJvm, store it in Blobstore, and record a capture on the lease — who requested it, when, why (caller, an admin, or the service itself), size, and duration.captures()lists them, and each is fetchable by its Blobstore reference long after the JVM is gone. Crash heap dumps (enableCrashHeapDumpis always set on a hosted JVM) are collected the same way when a JVM dies from out-of-memory, so a lease that OOMed at 03:00 has its dump waiting in the morning. The rawJvmProcess.getThreadDump()remains available for callers that want the result in-band rather than stored; the rawdumpHeap()returns a path on a host the caller cannot reach, so on a hosted JVM it, too, yields a Blobstore reference — how RemoteJvmApi expresses that without breaking local callers is an open question. - Thresholds are observations, never mitigations. An admin may set per-environment alerts (heap above a fraction of max for a window, thread count above a ceiling, deadlocked threads detected) that record an event and, optionally, take an automatic thread or heap capture — never restart or terminate the JVM. Surfacing the evidence is the service's job; acting on it is the owner's.
- Console output is a projection. Per the CQRS and state-management direction, the console is an append-only stream the WUI and CLI subscribe to rather than poll;
getConsoleOutput(fromIndex)remains for one-shot reads. The utilization series is served the same way.
9. Persistence and reconciliation
The Embedded is Event Log-backed: every lease transition (requested, parked, placed, renewed, terminating, terminated-with-reason), every provider install, every environment and account change is an appended event, and the lease list, quota ledger, and environment catalog are projections over it. This gives the audit trail an operator needs ("who held what at 03:00, and why did it die?") for free and keeps the service restartable.
On startup and on a schedule, the Embedded reconciles against each provider's reconcile(): a host the provider is running that no lease accounts for is terminated and recorded (PROVIDER_ORPHAN); a lease whose host the provider no longer has is closed with PROVIDER_LOST; and every live lease has its deadline re-armed in the sweeper. Reconciliation is what makes the provider-independent sweeper trustworthy after a crash, and it is the same discipline the droplet service applies to its fleet.
10. Deployment spectrum
HostedJvm sits on the standard deployment spectrum:
- Embedded —
HostedJvmEmbeddedwith the local provider is a library any program can use to lease and drive JVMs on its own host with deadlines and quota — the mode every test uses. - Self-hosted — the ServiceServer run standalone on a user's own box or VPS, with the local provider as the default environment and the user's own cloud accounts registered. Keys stay on the user's hardware, per the Decentralized by Default philosophy.
- Hosted — the operated instance on ContainerNursery at
url://hosted-jvm/, default environment DigitalOcean in shared-key mode, with bring-your-own-key open. ContainerNursery hosts the service; it never hosts the leased JVMs.
11. Frontends
- HostedJvmCli — in the MVP. User verbs mirror the manager (
request,list,get,renew,terminate) and the handle (consolewith follow,health,utilizationover a window,threadsandheapwhich capture and store,capturesto list and fetch them,invoke,stdin); anadminnamespace coversinstall-provider,register-account, and environment CRUD. Local mode runs the Embedded in-process with the local provider; remote mode connects tourl://hosted-jvm/, per the CLI architecture. - HostedJvmWui — a later milestone, following GOOD_DESIGN. The user face lists leases with a live deadline countdown, placement, a memory and thread utilization chart over a selectable window, the capture list with one-click heap and thread dumps, and a subscribed console; the admin face has an environments page and a keys page modelled directly on GithubProxy's — shared budget, registered accounts with validation state and usage, and an add-a-key form that forgets the credential as soon as the response is written. Stateless,
url://-only, per the WUI rules.
Prioritization — MVP vs. mature platform
MVP — the smallest thing that is honestly "a hosted JVM with a deadline":
HostedJvmApi,HostedJvmProviderApi,HostedJvmEmbeddedwith the local provider, leases, the sweeper on aClock, environments, per-caller quota with parking, Event Log persistence, reconciliation — all proven by end-to-end tests that lease real child JVMs, drive them throughJvmProcess, let aManualClockpass the deadline, and assert the process is gone.- The DigitalOcean provider over
url://digitalocean-droplets/, one droplet per lease, deadline mirrored into the droplet expiry, tested end-to-end against a real droplet in CI the way the droplet service tests itself. HostedJvmServiceServerdeployed to ContainerNursery aturl://hosted-jvm/with credential enforcement at the boundary;HostedJvmCliin local and remote modes; workloads from Maven coordinates and Blobstore JARs; utilization sampling withutilization(window)and stored heap and thread captures (§8); a ProductionHealth check that leases a tiny JVM, sees it healthy, and sees it die at its deadline.
Mature platform:
- Runtime provider installation from a Maven coordinate (the MVP ships providers compiled in, behind the same SPI, so installation changes no contract).
- The
url://aws-ec2/proxy and the AWS provider. - Kompile-built workloads from git; several leases packed per host; idle shutdown; port exposure.
HostedJvmWuiwith the keys page; W3Wallet allowances and settlement replacing the Embedded's own quota ledger; the console as a subscribed projection in every frontend.
Decision log
Decisions taken 2026-10-05, when this workstream was created:
- A new API family, not an extension of RemoteJvmApi. RemoteJvmApi remains the per-JVM control contract every provider implements;
HostedJvmManagervends aHostedJvmthat extendsJvmProcesswith lease metadata. JvmLaunchApi is untouched. See §1. - Workloads come from LambdaServer's sources plus "nothing". Maven coordinate, Blobstore JAR, kompile build rule from git, or a bare agent-only JVM — always with the RemoteJvm agent attached. See §2.
- Hard requirements are refused, never substituted; quota exhaustion parks, never rejects. See §3.
- The deadline is mandatory, defaulted to the environment's 1-hour default, capped by the environment, renewable only forward, enforced by a provider-independent sweeper, with the provider's native expiry as a second net. Idle shutdown is designed in but post-MVP. See §4.
- DigitalOcean goes through the droplet proxy; AWS waits for an
url://aws-ec2/proxy. HostedJvm never holds a raw vendor key. Bring-your-own-DigitalOcean-key means registering the token with the droplet proxy and handing HostedJvm the proxy credential. See §5. - Providers are runtime-installed Maven artifacts behind
HostedJvmProviderApi; environments are admin records. See §5 and §6. - Identity and keys follow GithubProxy's registration model today and map onto W3Wallet pools and allowances later, with no API change. Quota units are concurrent leases, total memory, maximum lease length, and GB-hours per day. See §7.
- The local provider is not the hosted instance's default. The ContainerNursery host is shared, saturated compute; the hosted default is DigitalOcean, one droplet per lease in the MVP, packing later. See §5 and §10.
- Persistence is the Event Log with startup reconciliation against every provider. See §9.
- HostedJvm is positioned as the general compute substrate; BuildTest, LambdaServer's executor pool, NetLabManager, and AiCliHostSupervisor are candidate consumers, not committed migrations. See relationship to other workstreams.
- CLI in the MVP; WUI later, with its keys page modelled on GithubProxy's. See §11.
- The service tracks utilization and owns dump captures. Health and memory are sampled by the service into a per-lease time series, and heap and thread dumps are stored captures in Blobstore with provenance, available after the JVM is gone; thresholds only observe and capture, never restart. See §8.
Open questions
- Heap dumps off-host.
JvmProcess.dumpHeap(outputPath, live)returns a path on the JVM's host. The hosted handle needs to return something the caller can fetch — a Blobstore reference. Does RemoteJvmApi grow an overload that takes a destination sink, or doesHostedJvmoverridedumpHeapto interpretoutputPathas a Blobstore name? The former keeps the contract honest for local callers too. - Requesting a runtime by version.
RemoteJvmBuilder.setJvmSpecpresumes the caller has enumeratedgetJvmSpecs()on the target host, which a hosted caller cannot do before placement. ArequireJvmMajorVersion(n)on the builder — satisfied by discovery at launch, failing loudly if absent — seems right, and benefits RemoteJvmLocal users equally. - Machine images. Does the DigitalOcean provider install a JDK on a stock image at placement time (slow, simple) or maintain per-JDK snapshots (fast, a cache to invalidate)? The MVP installs; the answer determines how "placing" latency is budgeted.
- Port exposure. A workload that wants to serve plain TCP/HTTP, not
url://, needs a firewall rule and an address the caller learns. Which providers can offer it, and is it a request field or an environment capability? - Several callers, one JVM. Can a lease owner grant another caller
console-only orinvoke-only access to their JVM? Natural once W3Wallet attenuation exists; until then it is probably "no". - Billing for self-hosted and embedded. As for LambdaServer: the hosted instance meters GB-hours; is a self-hosted instance expected to settle through the shared W3Wallet hub, or does it opt out?
- Sampling cadence and retention. How often utilization is sampled (per-lease RemoteJvm health calls are cheap but not free on a packed host), how long the series is kept after termination, and whether the per-environment aggregate is a stored projection or computed on read.
- Pending-lease lifetime. A parked request waits for quota; should it also carry its own "give up by" instant so a caller who walked away does not get a surprise JVM an hour later?
Relationship to other workstreams
- RemoteJvm (project) — the control contract HostedJvm vends and every provider implements; the two RemoteJvmApi additions above are this workstream's only asks of it.
- DigitalOcean Droplets (project) — the DigitalOcean provider's only route to DigitalOcean, and the model for lease expiry plus reconstructed fleet history.
- GithubProxy — the registration and keys-page model HostedJvm adopts for provider accounts, shared-key and bring-your-own-key alike.
- W3Wallet — the destination for identity, allowances, and settlement; HostedJvm's quota ledger is the stand-in until then.
- LambdaServer — shares workload sources (Maven, JAR, built-from-git) and the "a library first, a hosted service second" stance; its executor pool is a candidate HostedJvm consumer for isolation beyond the in-process child JVM.
- Pluggable Execution Environments and Kompile Remote Build — the
url://buildtest/droplet pool provisions its own droplets today; a HostedJvm environment is a candidate runner backend, and the build optimizer's memory-aware sizing is exactly aJvmRequest. Candidate consumers, not committed migrations. - NetLab and AiCliHostSupervisor — both provision hosts for their workers today; both are candidate consumers for the same reason.
- Blobstore, Event Log, Kotlin Build — workload bytes, persistence, and built-from-git workloads respectively.
- ContainerNursery — hosts the service, never the leased JVMs; the layering is the same one the LambdaServer decision log records.
- Manager–Daemon Pairing — a future "bring your own host" provider (a user enrolls a box they own as a placement target) is this pattern exactly; it is not among the initial three providers.
Graduation
There is no Documentation Repository project page for HostedJvm yet. This workstream graduates when the MVP tier is live: url://hosted-jvm/ on ContainerNursery leasing real DigitalOcean-backed JVMs with enforced deadlines, per-caller quota, the CLI, and a production health check watching a lease die on time. At that point add a Documentation Repository project doc and an ALL_PROJECTS entry, and it is no longer tracked as a workstream here.