Projects and execution targets
guestAccessOf(resolved) in src/brain/managedArtifacts.ts projects an already-resolved managed turn into GuestAccess for core file consumers. It copies the live provider, Project reference and account id without resolving, caching or authorizing again. Call it only after resolveManagedArtifactTurn or resolveManagedArtifactsFor succeeds. It performs no I/O and changes no authority; the provider still authorizes each operation. Consumers retain operation-specific options: resolveSessionContextFiles adds startIfNeeded: false, so opening a conversation does not start an environment. With no managed resolution the caller retains its existing host branch or explicit refusal.
ownSessionGuestSpillDir(sessionId) in src/brain/managedArtifacts.ts is the shared derivation of a conversation's guest tool-output directory. It applies the existing immutable session spill namespace to guestSpillDirForNamespace; rebinding a conversation does not retarget existing references. The helper performs no filesystem access, creates nothing and grants no access. persistStreamedToolOutput uses it only on its managed branch; host output keeps sessionToolResultSpillDir. Read claims remain limited by the existing artifact and Project authority rules.
The private writeImmutableSpill primitive in src/brain/session/toolResultSpillStore.ts shares create-only collision adoption between storeSpill and persistToolOutputSpill. It writes through the caller's existing writer and reads the survivor only on EEXIST; byte-identical text is adopted and unreadable or different content is a conflict. Other write failures propagate to the caller: clearing logs and retains inline context, while complete host output preserves its throwing error contract. Directory creation for complete output remains outside collision adoption. There is no registration or extra storage path; a successful fresh write needs no verification read on the host.
planStore.readGuestPlan and ensureGuestPlanExported project a resolved managed turn through guestAccessOf before bounded guest file operations. Use guestAccessOf(resolved) only after resolveManagedArtifactTurn has authorized the turn. It carries the same sandbox, project ref and account identity as the resolved turn and introduces no authority resolver. Managed-provider failures remain explicit errors with no host fallback; non-managed turns continue using the central plan. An absent guest plan may be exported from the existing central copy, while an existing guest copy is never overwritten by that export.
Project management
src/api/projectSummary.ts builds the display-only Project register summary from rows
already authorized and placed in account order by src/api/routes/projects.ts. Call
projectSummary(ctx, allowed, user, admin) only after that route's tenancy filter.
Plugins receive only this allowed batch; foreign indicator ids are discarded, with at
most three indicators per plugin/project and eight per project. Registry reads retain
their five-second budget and each provider its two-second budget. Member samples stay
admin-only and capped at sixteen. Managed branches retain four concurrent readers, a
four-second batch budget and the shared thirty-second memo; absent or failed providers
omit the branch instead of reading host storage. The web Project cards are the real
consumer. No projects means an empty summary, with no member or branch rows fabricated.
Minimal use: a route calls the Project service with the caller as an actor and a body thunk,
as src/api/routes/projects.ts does for create (service.create(actor(c), () => c.req.json())).
The service is the single implementation of every Project operation an account can perform,
so a new caller goes through it rather than repeating the tenancy checks.
plugins/sandbox/lib/environmentRuntime.mjs composes managed Project lifecycle work and owns admission, live authorization, the reconcile queue and detach ordering. environmentResources.mjs owns usage validation, persisted snapshots, generation/epoch-qualified refresh promises, history and disk-ceiling enforcement. The runtime authorizes the whole usage batch before passing it to readUsageBatch, and rechecks history membership immediately before readHistory; neither path provisions a machine. The existing sampler admits at most 1,000 environments per sequential probe and commits snapshot/history groups of at most 32. Missing or failed measurements retain the existing unavailable/stale presentation and diagnostics. Detach waits for accepted refreshes before publication mutations.
environmentAlerts.mjs projects those stored disk readings and recovery exhaustion onto ctx.alerts. It keeps the existing 80% warning and 75% clear marks, raises start-refusal conditions at admission and clears them on resolution or deletion. Without a host alerts contribution it emits nothing; emission failures are logged without failing lifecycle work. environmentPublications.mjs owns socket naming, per-publication mutation promises, durable binding and retirement cleanup, and recovery-state reporting. The runtime calls publication reconciliation after Project lifecycle work and drains admitted bindings/releases on detach. A missing forwarder is restored from its durable binding; a retired identity remains fenced before and after an in-flight start. Sites and previews consume the same binding control, and publication probes stay batched by machine.
Host runtime readiness and the Host runtime settings panel share one ordered items list. Root filesystem artifacts retain their verified download cache; no automatic artifact garbage collection runs. Failed downloads still remove their own incoming file, and every reused blob is verified before materialization.
The catalogue publishes only the current recipe revision. Existing materialized disks retain their source image and disk identity independently of the artifact cache: daemon boot reconciliation, explicit restart and host boot recovery prove the existing envelope and reuse the disk without looking up a retired pin. Network unit enablement is baked into the archive; no runtime normalization RPC changes existing roots. environmentRuntime.mjs::assertImageReplacement serves both request admission and recovered durable recreate/restore intents, refusing a retired environment or snapshot with retired_image before quiescing writers, stopping a machine or changing its specification. Restore copies only current-image v2 snapshots and retains their source image provenance without relabeling. retiredImages.test.ts covers the deployed @1, @2, @16 and @18 families, pending/running intent recovery, unchanged data and disk identity, and absence of downloads.
Runtime integration tests live in tests/plugins/environmentLifecycle/: lifecycle admission, start/stop, snapshots, recovery, deletion, execution leases, file binding, adoption rollback, desktop stop, machine-host readiness and concurrency have separate suites. resources.test.ts and diskCeiling.test.ts exercise the resource owner, publications.test.ts exercises durable publication ownership, and rootfsArtifacts.test.ts exercises artifact binding; alert coverage remains in tests/plugins/environmentAlerts.test.ts. fixtures.ts shares the existing runtime setup, cleanup queue and common inputs, with an independent in-memory database, scratch directory and mocked machine client per setup. Vitest isolates the fixture module per test file, and its afterEach drains only that file's cleanup queue. Guest-file and resource helpers used by one suite stay local to that suite.
Within web/modules/projects/, ProjectCard composes ProjectTeamStrip and ProjectLocationTip
for ProjectsView. The strip owns membership presentation, measured overflow, its non-passive
wheel listener and mouse-edge animation with blur, leave and unmount cleanup. It consumes only
horizontal paging keys and clicks on the strip or count, preserving the card's other navigation.
Absent membership renders nothing; a served empty team stays screen-reader-only. Cost is linear
in the bounded member sample, with one resize observer and animation frames only during drift.
The location tip owns the host/managed location disclosure and its hover, focus and tap state;
it reads only the supplied project and resolved labels and performs no fetches. Both retain the
existing tooltip, avatar, motion-preference and horizontal-wheel UI seams.
Sandbox owns managed Project resource history in lib/resourceHistory.mjs, through its existing plugin database and ten-second due-work clock. One detached refreshResources path serves current cards, history and disk-ceiling enforcement: stable running environments are sampled every minute, stopped or failed materialized environments every five minutes. Active lifecycle operations and deleting or unmaterialized targets leave gaps. Nspawn reads timestamped cgroup counters before awaiting its coalesced five-minute disk-tree cache; disk history carries the oldest physical observation time. Disk transport failures and invalid helper reports reject the resource batch into refreshResources' existing sandbox.resource_usage_failed diagnostic, leaving the durable snapshot and history unchanged. Successfully returned unmeasurable trees produce unavailable readings only for their environment, with prior card readings retained as stale.
Sandbox's internal lib/nspawnMeasurement.mjs owns pure conversions of cgroup CPU, IO and memory counters, systemd timespans and the MiB disk ceiling. NspawnClient imports these directly for ownership-limit checks and resource batches; host reads, timestamps, caches and privileged operations remain in lib/nspawn.mjs. Invalid CPU/IO/memory counters retain their errors, an empty IO report means zero traffic, and malformed timespans return null. Parsing costs only the supplied text scan and performs no I/O. The runtime client's disk surface keeps ownership-checked remove(spec), removeStorage(spec) and removeSnapshotStorage(spec, savedManifest); incomplete snapshot receipts remain recovery evidence. The real nspawn proof uses stop(spec) followed by remove(spec) before checking liveness and removing storage.
The operation store projects updated_at_ms in its bounded recentOperations read through SQLite's UTC date parser. Disk-ceiling cleanup grace reads that value for the first successful start after a ceiling stop; both existing SQL timestamps and millisecond ISO operation timestamps keep the same fifteen-minute window. This is a read projection over existing operation rows, not a second timestamp store or migration.
The current snapshot's generation, refresh epoch and refresh-start CAS must accept before history is appended. Fresh probe values enter history before the card retains prior unavailable readings. Snapshot, selected minute observations and hourly aggregates commit atomically in synchronous IMMEDIATE writer transactions covering at most 32 environments. Validation and probes occur before locking; no await, filesystem walk or retry loop runs under the writer. Failed persistence is logged and does not bypass generation-fenced disk enforcement.
Each Project/series/minute contributes at most one sample and updates its UTC-hour aggregate once. Recreation within a duplicate minute or an hour marks discontinuity without fabricating zeroes. Minute detail lasts 48 hours; hourly mean/min/max and original measured percentages last 30 days. Live reads enforce those horizons independently of pruning. Each maintenance tick deletes at most 1,000 expired or project-removal rows in a separate short transaction, with a durable deletion queue and no backlog loop or VACUUM.
GET /plugins/sandbox/api/environments/usage-history uses the plugin-owned resourceHistoryTypes.d.ts contract. The authenticated Sandbox grant and request project scope precede a fresh runtime membership/managed-Project gate immediately before synchronous indexed reads. Reading never provisions or refreshes. Current members, including read-only accounts, share retained Project history; revoked, foreign, host or deleting targets are refused. The strict handler validates types, exposed parameter names, timezone-qualified instants, bounds, resolution and metric grammar from the host's existing query object. Live window=1h|6h|24h|7d|30d is mutually exclusive with fixed custom from/to; unknown windows and mixed forms are rejected. The server resolves exact elapsed durations against its own now and includes the current partial bucket; omitted windows and bounds select live 24h. Client clocks never anchor live history. The fake daemon shares this boundary and range calculation rather than approximating bucket rounding. Repeated query keys follow the host's existing projection. Valid ordered windows are clamped to server now and its 30-day horizon; the response preserves requested bounds and marks adjustments. A window with no retained overlap returns 400 with range_outside_retention. Requests select at most five series and 720 time buckets each; older ranges use complete stored hours plus the current partial hour and report effective bounds. The metrics parameter takes at most four names, and io expands to two series, ioRead and ioWrite, which is how a request reaches five.
The Sandbox administrator account-environment route, environment GET, returns mode, probe, home, author and migrationCollision. Its browser declaration is EnvironmentState in the plugin's web-src/runtime.ts; the user drawer (web-src/EnvironmentSettings.tsx) reads mode for host-execution policy and probe for readiness. Network reachability is not a separate response field. This read never selects a managed Project runtime.
Sandbox's internal lib/workspaceTransfer.mjs owns the workspace inventory and the verified cross-filesystem copy used by ContainerStorage.adoptWorkspace and releaseWorkspace. Callers retain their preconditions and atomic same-filesystem rename; on EXDEV they supply the existing staging path, error messages and target-restoration policy to copyWorkspace. It checks destination free space, hashes the source before and after copying, verifies the staged tree, syncs files and directories, activates staging, and only then removes the source and syncs its parent. Adoption restores an empty target after capacity/copy failure; release leaves it absent. Cleanup failures preserve both errors and take precedence over restoration. Existing nonempty-target and completed-transfer behavior remains caller-owned. Inventory walks retain sorted paths, file sizes/content digests and symlink target bytes; copying uses Node's existing timestamp-preserving, verbatim-symlink primitive and propagates its refusals. The shared syncPath also serves disk and snapshot durability. These are private storage primitives, not execution authority: the environment lifecycle must exclude writers. No transfer does any work unless invoked; an EXDEV transfer costs a full copy and three inventory walks, with no byte cap or privileged runtime call.
The account HOME reset routes in lib/api.mjs share the internal homeResetPreview(userId, state). It builds the payload and SHA-256 from already loaded state, serializing userId, generation, bytes, entries, activeProcesses and author in the existing order. Neither route performs extra state reads; reset still checks the hash before the typed phrase and rechecks generation/active leases under the existing HOME lease. runPrepared clears its heartbeat and releases its execution lease unconditionally in finally, including launch failure. lib/runtimeProcess.mjs exposes only bounded completion capture for SpawnExecutor: independent stdout/stderr tails, truncation, exit status, timeout and abort. Nspawn is its actual consumer; there is no streaming observer.
Sandbox's artifact builder owns immutable revision publication through pinsFrom: a generated candidate such as { reference, digest, sizeBytes, path } must agree with an already published pin. buildRecipe measures staged compressed bytes before replacing an output archive, and writePublication validates the entire manifest before writing either metadata file. Identical rebuilds, new revisions and unpublished first publication are accepted. Rejection leaves prior archive and metadata bytes intact; this local validation does not upload assets or make the two metadata writes a filesystem transaction. The constant-size pin comparison is reused at both write boundaries. The builder's internal test entry accepts a tiny recipe, command executor and pin table so regression tests never bootstrap a real image. CLI --verify passes verification mode into the same buildRecipe: it measures and inspects temporary staging, skips publication checks and archive copies, and returns metadata with output: null for verifyArtifacts to report matching or differing bytes. Verification leaves output directories, archives, manifests and pins untouched. Standalone recipes produce their own provenance; manifest entries omit the unused base and baseRecipeDigest fields without changing recipe revisions or archive pins. The streaming tar/PAX reader lives in plugins/sandbox/tools/lib/tarReader.mjs, consumed directly by the builder and its archive fixtures; it retains GNU long names, hardlinks, base-256 fields and bounded member capture. scripts/build-rootfs-artifact.mjs remains the stable CLI named by generated pins, with no test-export barrel. Real-disk proof callers pass source/target specifications to disk operations and an explicit Project tree owner to direct fingerprint requests, so a host-path refusal is exercised with valid authority.
The background operation dock reads Sandbox's existing durable p_sandbox_runtime_operations rows. Migration 16 adds initiator and acknowledged_at; the full human HTTP identity stamps ui, turn credentials stamp agent, and automatic work stays system. Core Project create/delete and execution selection pass this provenance through the existing lifecycle control. GET /plugins/sandbox/api/environments/operations returns { operations }, limited to the caller's own UI work and unseen settled outcomes from the last 24 hours, with fresh managed-Project access checks. A successful own deletion remains readable after Project removal. POST .../operations/ack { operationId } confirms only own settled rows, idempotently, and publishes an owner-targeted frame so a second tab drops it even after deletion. Running acknowledgement returns 409, foreign or inaccessible work 404. The shell's UI API 54 useOperationDock shares this list and pushed rows; no core table or browser storage duplicates operation state. No Sandbox means no environment source. Opening progress uses the existing single-operation reader; see docs/WEB.md for the view, timing and accessibility contract.
Every Project operation an account can perform has one implementation, createProjectService in src/projects/projectService.ts. The HTTP routes (src/api/routes/projects.ts for create, edit, adopt, delete, order, members and memory members; the membership writes in src/api/routes/users.ts) and the agent's Project tool (src/brain/tools/projectTool.ts) both call it; the routes get it on RouteContext.projectService, the brain on BrainDeps.projectService (built in buildBrainCore, so a forked sub-agent runner has it too). The service authorizes through the shared tenancy predicates in src/api/access.ts and validates bodies with the API's own zod schemas from a body thunk, so the routes keep reading their body only after the existence and authority checks. A refusal is a ProjectOperationError carrying the status and body the API always answered with; routes render it verbatim and the tool reads the same message. A caller is a ProjectActor: the account, plus for an agent turn the turn's live Policy.allowedProjectIds (a delegated child's narrowed scope), which narrows every Project-scoped operation on top of the account's authority. That scope is resolved before the turn runs and drops a Project once its deletion begins, so the actor also carries conversationProjects: the Projects the tool's conversation created or began deleting through the service, which the service records and adds to the scope for management only (its own checks, and projectIds during its provider calls). The conversation can thus start and manage the Project it just made and follow the deletion it started; work inside a Project still sees only the policy, and a delegated child gains nothing, since it may not create and what it deletes was already in its scope. A managed environment stays the Sandbox plugin's: the service reaches it only through the sandbox control (environmentFor, environmentSnapshots, requestEnvironment, environmentOperation, revokeProjectAccess, releaseAdoptedWorkspace), and the provider validates limits, network, ports and snapshots and decides who may change them. Sandbox's containerSpec.mjs::normalizeContainerLimits owns the arithmetic and closed resource shape for request admission, provisioning defaults, specification creation and live updates. Call it with partial limits such as { cpus: 2.5, swapMb: 0 }; absent ceilings use DEFAULT_CONTAINER_LIMITS, and supplied invalid values throw before enqueueing. Stored reads retain their existing missing-field projection, and live deserialization retains its nullish-default rule. The durable operation executor revalidates limits through that same helper before checking disk capacity or changing pre-start intent or live limits. Invalid recovered operations fail without changing the stored valid limits, so the environment remains readable. The constant-size validator requires whole CPU microseconds and bounded integer ceilings; environmentRuntime separately checks the measured disk volume. Positive numeric provisioning defaults are validated instead of silently replacing the whole set. Those calls run under runAsProjectManagement (src/plugins/policyContext.ts), which stamps currentAccess().projectManagement: inside a turn the provider then judges a Project the tool names by membership and the turn's project scope rather than refusing everything but the conversation's selected target, as the HTTP API does. Outside a turn the marker changes nothing.
Absent wiring: without the tool's dependencies the Project tool answers that Project control is unavailable (src/brain/tools/projectTool.ts:312). Without BrainDeps.projectService (src/brain/brainDeps.ts:69), list and switch still work and every management action answers that Project management is unavailable (src/brain/tools/projectTool.ts:347).
The Project tool keeps one flat, provider-compatible object schema with an action discriminator; the strict zod union inside execute is the validator. Actions: list, get (the Project plus, for a managed one, its environment and snapshots), switch, create, update, delete, order, members, add_member, remove_member, memory_members, adopt, start, stop, restart, recreate, resources (unnamed ceilings keep their current value, because a limits action replaces all six), network (mode and the full inbound port list), snapshot, restore, delete_snapshot (the Environment tab's durable deleteSnapshot action), operation, logs (the lifecycle log and guest journal, each cut to its newest 16,000 characters), worktrees (listing never starts the environment), worktree_create and worktree_remove. Project is the only agent path to a managed environment; Sandbox registers no environment tools of its own. Environment actions answer a pending operation. Plan mode refuses the whole tool through the plan-mode deny set (it is not plan-safe) and the tool refuses a mutation in plan mode itself. Read-only delegated children receive it through READ_ONLY_AGENT_TOOLS for its reads (list, get, members, operation, logs, worktrees), and the tool refuses every other action when the turn carries a read-only origin. A delegated child may not switch, create or order. delete, restore, delete_snapshot, adopt with undo (it removes the environment's containers, storage and snapshots) and a smaller diskMb confirm through resolveAsk (src/brain/toolPermissions.ts), the same resolution an ask permission rule takes in the execute-time gate: a prompt where a human is attached, YOLO and the unattended setting elsewhere. They ask even after an ask rule approved the call, because the gate's prompt for a non-shell tool names only the tool. stop, restart, recreate and network need no confirmation: each replaces only the machine envelope and keeps the root filesystem and project storage. A resources change re-reads the ceilings after its confirmation, because the provider takes all six at once and a ceiling change does not move the generation; a disk ceiling that changed while the person was answering refuses with limits_changed. Results are compact projections (no avatars, e-mail or host storage paths).
A conversation's own target never outlives its project. ProjectStore.removeRows, the one deletion path for host projects and for managed ones after verified runtime cleanup, clears every brain_sessions.execution_ref that names the project in the same write transaction, so the conversation has no explicit target again instead of reporting a project id that will never exist (ids are not reused). Migration v45 cleared the refs earlier deletions left behind. A delegated child's target lives in its immutable delegated_access scope and is not rewritten; effectiveTurnWorkDir resolves a managed target whose project is gone to the host base directory.
Version 38 (src/store/sessionEventsToJournal.ts) retired the old side table brain_session_events. Each setting marker is threaded into its conversation's journal as a chat event before the first person message stored at or after it, or at the tail, which for a branch ending on a provisional run is just before that run; later entries are re-parented and their seq shifted, and the leaf and next_seq follow. A threaded model switch names the provider of the first later reply that this model produced, or the session's provider when it is still the session's model; when neither proves it, the event names the model id alone (provider is optional on the model event). Subagent and workflow markers are not threaded: their finish is backfilled onto the delivered result it belongs to from brain_subagent_results. Stored sessionChanges prompt frames are stripped, and the table is dropped. Fresh databases below v38 stage an empty table for the historic v5, v9 and v10 rebuilds.
ProjectService.authorizedRows(actor) returns stored project rows in the account's canonical order without filesystem projection. It selects visibility and order through UserProjectStore.ordered once per call: administrators see all rows, other accounts their current assignments. With either account store absent, open mode returns all rows; with both stores present, an absent account returns no rows. Use service.authorizedRows({ user }) before projectSummary(ctx, allowed, user, admin). The summary route and ProjectService.list/get share this selection; list/get attach their existing filesystem projection afterward. inAccountOrder is private. Account-row selection does not replace the session policy or the existing turn-scope checks for project-scoped reads and mutations.
projectSummary returns the canonical ProjectSummary wire contract. Its indicator map uses ProjectSummary['indicators']; the exhaustive ProjectIndicatorTone dictionary validates incoming tones using Object.hasOwn. The registry is resolved once per summary with the existing five-second timeout and diagnostic. Managed branch batching receives the Sandbox control from that resolved registry rather than loading it again. A failed or timed-out registry leaves indicators absent and skips uncached managed branch reads, while already memoized branches retain their existing behavior. Each indicator provider retains its two-second budget and output bounds of three indicators per plugin/project and eight per project. Managed branch reads retain four concurrent readers, a four-second batch budget and the thirty-second memo; member samples remain administrator-only and bounded to sixteen. Empty authorized rows produce an empty summary. Project register cards are the real consumer.
Project HTTP actors derive lifecycle-operation provenance through operationInitiatorFromCredentialScope from the host-verified credentialScope. Full and impersonation credentials map to ui, agent credentials to agent, and api/advisor or absent credentials to system. Caller bodies cannot select provenance. This pure classifier adds no I/O and does not change direct-tool defaults; project creation and the existing environment operations consume the actor.
Both lifecycle restoration and periodic publication reconciliation use the same per-identity locked recovery block in environmentPublications.mjs. The block re-reads the durable Project binding after entering the lock, skips bindings that were removed and forwarders already proved active, and starts missing forwarders using the current binding's port. Lifecycle restoration checks active units directly; periodic reconciliation retains one batched status read rather than issuing a host call per publication. Status failures retain caller-specific recovery logs and do not stop the Project. No durable binding means no forwarder to restore. Sites and previews consume this through the existing projectPublicationBinding control.
configuredLimitMeasurements(limits) also owns configured resource denominators: memoryBytes and diskBytes convert MiB using 1024 squared, ioBytesPerSec converts decimal MB/s, and networkBytesPerSec converts decimal Mbit/s, with zero bandwidth represented as null. NspawnClient.resourceUsageBatch and environmentResources' unavailable-resource projection consume the same constant-cost conversion; state, observation time and probe availability stay with the callers. With no measurement, configured denominators remain available and usage remains null. This helper accepts normalized limits and performs no host reads.
RuntimeClient in lib/runtimeClient.mjs documents the complete injected NspawnClient contract. Resource consumers call diskCapacityBytes(spec) for the host-derived filesystem capacity and resourceUsageBatch(entries) for one to 1000 ordered observations. An unreadable capacity is null at the client boundary; writing a disk ceiling refuses it, while a read may display an unknown bound. Failed resource transport rejects into the existing refresh diagnostic. Host readiness, provisioning, networkReadiness and waitForNetwork are required methods; incomplete injected clients are not an alternate runtime mode. ensureLiveLimits and ensureNetworkLimit keep independent applied/refused caches with five-minute refusal retention. Shared-link checks observe the leader before checking the leader/rate key; isolated environments make no network-limit call. Lifecycle start, live updates and daemon reconciliation are the existing consumers.
lib/resourceHistoryMetrics.mjs owns the current resource-history vocabulary: cpu, memory, disk, io and network expand through resourceHistorySeriesIds(metrics) into the seven wire series. RESOURCE_SERIES states each series' physical source, value field, limit field and unit; CPU receives its limit from the caller. The server sampler and history response, ResourceHistoryMetric/ResourceHistorySeriesId types, browser response validation and chart selection derive from this vocabulary. Shipped migration CHECK literals remain frozen, and UI labels, icons and colours remain local. With no requested selection, the HTTP history boundary selects all five resources; missing observations keep the existing explicit gaps. The vocabulary performs only bounded selection work, makes no requests and adds no metric registration mechanism. ProjectUsageHistoryModal is the browser consumer.
ProjectExecutionInvariantError represents refusal of an unavailable or unusable execution target. Callers distinguish it with instanceof; its name and contextual message remain available, and it has no separate code field. preparePersonalProject throws it when a restricted account has no usable default Project, without creating a conversation or selecting the host. The class does not recover, provision an environment, or alter the selected target.
The managed branch reader consumes the existing runProjectCommand lifecycle through managedGitReader. A SandboxCommandError whose reason is timeout records an unavailable branch locally and retains the short failed-read memo. Caller cancellation remains local-only; unexpected environment and command failures retain project.branch_read_failed. No branch read starts an environment, changes authorization or adds a second execution lifecycle.
Network measurement and enforced ceilings
Every environment is also held to a disk speed ceiling, one figure for reading and one for writing, so a single environment that copies or builds heavily cannot make the host's other services wait for the disk. The built-in ceiling is 150 MB per second (decimal, the way disk makers and systemd count), about what a 7200 rpm hard disk sustains, and it applies to every environment that has no ceiling of its own, including those created before it existed. The plugin default (defaultIoMbPerSec) is what a newly created environment starts with, and administrators change it per Project on the Environments page, or with the Project tool's resources action (ioMbPerSec). It is a property of the running machine unit, not of its identity: it is applied before each start, on a live change and, once per daemon boot, to machines that were already running, so it never causes a rebuild. Environments are also kept out of the host's swap. The swap limit is the amount of swap an environment may use beyond its memory limit, zero by default, so an environment that reaches its memory limit has processes stopped inside it instead of pushing its pages onto the host's disk, where every other service would feel it. The plugin default (defaultSwapMb) is what a newly created environment starts with, and administrators raise it per Project on the Environments page (the row above the disk speed), or with the Project tool's resources action (swapMb, in MiB). It is applied together with the disk speed, in the same way and at the same moments. On a Project's card the five rings show CPU, RAM, disk space, disk speed and network. Network shows the busier of guest download and upload in decimal Mb/s, with both directions in its tooltip. An unlimited network has no percentage arc; an isolated running environment reports zero external traffic. The history picker includes Network, plotted in Mb/s on the right axis. The daemon reads kernel host0 counters through the machine leader's network namespace, resets rate baselines on leader or generation changes, decreasing counters and gaps over two minutes, and retains both directions as netRx/netTx in the existing minute/hour history store. Migration 17 expands the stored metric enum without discarding existing history. The sampler uses one read-only machine-manager call per running shared environment, no guest commands or privileged operations. The disk speed ring is the busier of read and write over the last sampling interval, as a share of the ceiling, so a full ring means the environment is being held back by it; its figure is the busier direction in MB/s (two decimals under 10, one under 100, whole numbers above), and hovering it names both directions. The host resolves the limit to the block device that holds the sandbox data directory, so the data directory must live on a device the kernel can account (an ordinary disk or partition).
The Network speed resource row and the Project tool's resources action (netMbitPerSec) set an independent ceiling for download and upload. Zero is unlimited for both new and existing environments; the Project tool accepts whole numbers up to 100,000 Mb/s, while the slider offers 0–10,000. A positive rate changes live without a restart and stays outside the machine identity. Before boot, the helper saves a per-machine systemd start hook outside the creation identity. Every real unit start, including an in-guest reboot, must apply the current ceiling to its newly registered link before systemd reports the unit active. A hook failure fails the unit start. The daemon also waits for registration and active state, and Start retries or resumes verify both disk/swap and network ceilings before success. The running-machine reconcile cache remains keyed by leader and rate. Refusals are held for five minutes and remain visible in the environment log. Isolated networking has no external link to limit, but retains the setting for a future shared network.
Host provisioning also installs iproute2, which provides the fixed ip/tc commands used for network limits; readiness reports it beside systemd-container. The root helper operation set-network-limit derives the systemd veth from its full alternate name, verifies both peer interface indices through the registered leader's network namespace, and accepts no caller-provided interface. Download uses a token-bucket queue on the host veth. Upload is redirected from its ingress to one root-created IFB with another token-bucket queue. That IFB's name is a digest of the machine name and its alias identifies its owner; an existing foreign link is refused. Returning to unlimited, stopping or destroying the machine removes its IFB. Stop still enters that ownership-verified cleanup after an in-guest poweroff has already stopped the machine. The mandatory unit hook calls apply-network-limit, which reads the latest configured ceiling under the environment lock and shares all interface/ownership checks with set-network-limit; it never writes an old start-time rate over a live edit. The hook imports the same root-owned helper by its own module URL. Existing helper byte comparison and worker digest checks therefore cover it, and privileged refresh updates it without adding an independently installed script. The queues permit a 64 KiB minimum burst or 20ms at the selected rate and a 100ms queue; rates are averages, not a promise against every individual packet burst. A disposable namespace proof with a 10 Mb/s cap and 8 MiB TCP transfers measured 9.64 Mb/s in both directions, with no drops. The simpler ingress policer measured only 2.54 Mb/s upload, so it was rejected. Removing the queues restored unrestricted transfers. Deployment needs the daemon build, privileged helper refresh, web build and then restart, in that order.