- Status: done
- Date: 2026-06-19
- Specs touched:
ENVIRONMENT.md# Per-instance resource limits (wording synced to the locked runtime model),APP_ISOLATION.md# Resource limits (governing spec — implemented, not changed),NEXT.md(disk-quota deferral)
The C6 slice of the cloud-VM track (#196, #211) and the first lifecycle seam to actually branch on the environment profile, building on hosted-profile-marker.md (C1a) — whose "what's next" flagged that the marker was read-and-logged but consumed by nothing yet. This is the first consumer: a per-app cgroup-limit policy on the install/reconcile transaction, with CPU capping gated to hosted.
#211 and ENVIRONMENT.md's first-pass wording said per-app limits are "sourced from the manifest's declared resources" and apply --memory / --cpus to "both profiles." That contradicts the locked APP_ISOLATION.md # Resource limits:
- The manifest cannot impose a limit — there is no
limitfield, by design (an author can't see the user's hardware). It declares onlyrecommendedspecs (advice, never a ceiling). So the limit is not manifest-sourced. - CPU is never capped on the appliance — it is time-shared; throttling only makes an app feel sluggish. The appliance's only cap is the optional, default-off, user-set memory cap.
Resolution (no locked decision flipped, so no DECISIONS.md entry): keep the manifest limit-free, make the mechanism profile-divergent — appliance caps memory only, hosted caps memory + CPU — and sync ENVIRONMENT.md to match. The control-plane API and user-cap UI that would set a policy do not exist yet; this PR builds the store-backed policy + the setter seam + the application path, nothing user-facing.
internal/store/(newresources.go+ migration) — a newinstance_resource_limitstable (PKinstance_id→instancesON DELETE CASCADE,memory_bytes+nano_cpus, bothDEFAULT 0).ResourceLimitsvalue type withIsZero;GetResourceLimits(a missing row is the zero value, not an error — the uncapped common case) andSetResourceLimits(upsert; the FK rejects an unknown instance). The cascade means uninstall reclaims the row with no extra teardown step.internal/lifecycle/(newresources.go) —Manager.SetResourceLimits(id, lim)is the seam a future control-plane push / user-cap UI calls: it validates non-negative, checks the instance exists (store.ErrNotFoundsurfaces), and persists. No production caller yet, by design.resourceLimitsStanza(profile, lim)renders the composedeploy.resources.limitsblock — memory in both profiles,cpusonly whenprofile == hosted(a stray appliance CPU value is dropped, not rendered),nilwhen nothing applies (the app bursts freely).reapplyResourceLimits(id)surgically patches only the main service'sdeploystanza in the already-renderedcompose.override.yml, reporting whether it changed;mainService(id)reads the persisted manifest for the service name.internal/lifecycle/lifecycle.go— the CPU-cap gate reads theManagerprofilefield (the profile-divergence seam C3b/#207 added viaSetEnvironment, defaulting toAppliance);writeOverridetakes the resolvedResourceLimitsand stamps the stanza on the main service at install;Reconcile's running branch re-applies the policy — on a drifted instance it patches beforecompose up, on a live instance it recreates the container only when the stanza changed, so a policy edit (or a clear) takes effect without a reinstall and an unchanged policy is a no-op.cmd/brain/main.go— no new wiring: the CPU-cap gate reuses theprofilealready set at startup by C3b'slife.SetEnvironment(prof, boxID)(from the marker C1a reads), so it reflects the box's posture.- Docs —
ENVIRONMENT.md# Per-instance resource limits synced to the locked model (limit is user-/control-plane-sourced, not manifest; memory both profiles, CPU hosted-only; disk quota deferred).NEXT.mdgains the disk-quota item.
- Disk quota (deferred, in
NEXT.md). The third hosted dimension. The lockedext4+overlay2stack can't enforce a per-container quota portably; the path is XFS project quotas through the host-agent (new protocol verb +host-agent-realop + adisk_bytescolumn on the same store seam). Filed as #221. - A caller for the seam.
SetResourceLimitspersists a policy but nothing invokes it yet. Two future consumers: the appliance's user-set memory cap (APP_ISOLATION.md# Optional user-set cap →DASHBOARD.md/LOCAL_ANALYTICS.mdapp-hog signal) and the hosted control-plane policy push (ENVIRONMENT.md# Deferred commercial layer). - Immediate application. Today a set policy lands on the next reconcile (brain restart) or the next drift recovery. A real caller will likely want to apply it inline (call
reapplyResourceLimits+compose upfor that instance) rather than wait for reconcile; that belongs with the caller, not this seam.