# Declarative Resource Limits: Design Alternatives (https://jackin.tailrocks.com/research/agents/orchestration/resource-limits/declarative-resource-limits-design/)



**Research state:** Reference

## Summary [#summary]

Operator-level `[docker.grants]` should remain the resource authority because roles and repositories must not grant themselves host capacity.

## Research question [#research-question]

jackin❯ runs every agent in a Docker container with host-defined resource allocation. On a developer laptop, six parallel agents can each spawn a `cargo build`, exhaust memory, OOM-kill the desktop, or starve each other for CPU. The current operator surface is `[docker.grants]`. Docker exposes the right primitives (`--memory`, `--cpus`, `--ulimit nofile=N`, plus `--memory-reservation` for soft limits); the design question is how much jackin❯ should abstract away from Docker's specific flag names.

This matters beyond a single-host nicety: the [autonomous task queue](/roadmap/autonomous-task-queue/) is unusable without some resource ceiling — five queued agents with unlimited memory and CPU is a recipe for simultaneous OOM kills the moment two of them run `cargo build` at once. And cross-backend resource translation (Docker, Apple `container`, and any [selectable sandbox backend](/research/platform/security/sandbox-backends/)) each express limits differently, so a declarative layer that translates once per backend looked like the right shape regardless of where the config surface lived.

## Inspiration in multicode [#inspiration-in-multicode]

**Sources**:

* README — [Isolation](https://github.com/graemerocher/multicode#isolation)
* Config — [`config.toml` `[isolation]` block](https://github.com/graemerocher/multicode/blob/main/config.toml) (`memory-high`, `memory-max`, `cpu`)

multicode addresses this with three declarative fields plus an FD ceiling:

```toml
[isolation]
memory-high = "12 GiB"   # soft limit; triggers cgroup memory.high
memory-max  = "16 GiB"   # hard limit; triggers OOM at this point
cpu         = "300%"     # 3 CPU cores worth of quota
nofile      = 16384      # FD ceiling (Apple container only)
```

multicode parses these via the `size` crate (decimal `12 GB` and binary `16 GiB` both supported), expands shell variables, then maps them onto `systemd-run --property MemoryHigh=...` etc. — each backend has its own translator, but the config surface is uniform. multicode also tracks runtime metrics that complement the limits: current RAM, CPU %, and crucially **OOM kill count** (sampled from the systemd memory-pressure counter), so the operator sees an OOM kill without digging through logs.

jackin❯'s shipped `[docker.grants]` (`memory`, `memory_reservation`, `cpus`, `pids`, `nofile`) is the same shape as multicode's `[isolation]` block, field-for-field, minus the `-high`/`-max` naming (jackin❯ uses Docker's own `memory`/`memory_reservation` names instead of multicode's backend-neutral `memory-max`/`memory-high`).

## Design alternative not adopted: role-manifest `[runtime.limits]` [#design-alternative-not-adopted-role-manifest-runtimelimits]

One design direction considered putting resource defaults in `jackin.role.toml` instead of (or in addition to) operator-level config, on the theory that limits scale with the toolchain a role installs (a role that builds Rust from source needs more memory than one that just runs a linter):

```toml title="jackin.role.toml (not adopted)"
version = "v1alpha2"
dockerfile = "Dockerfile"

[runtime.limits]
memory_high = "12 GiB"     # soft (Docker --memory-reservation)
memory_max  = "16 GiB"     # hard (Docker --memory)
cpus        = "3.0"         # Docker --cpus (string for "1.5", "300%")
nofile      = 16384         # Docker --ulimit nofile=N:N

[runtime.limits.oom]
preserve_state = true       # don't auto-clean an OOM-killed instance
notify         = true       # surface OOM in console
```

A one-shot CLI override was sketched alongside it (`jackin load <agent> --memory-max 8GiB --cpus 2.0`), and a per-backend `ResourceLimits` translator trait: Docker (`--memory`/`--memory-reservation`/`--cpus`/`--ulimit nofile=N:N`, plus `oom_score_adj` for preserve-state), Apple `container` (direct per-allocation limits, once [selectable backends](/research/platform/security/sandbox-backends/) ships), and `systemd-run`/`bwrap` (cgroups properties, if either ever lands). The intended contract: a backend that can't honor a declared limit (e.g. `nofile` on a runtime that doesn't expose it) emits a warning at launch and proceeds, rather than hard-erroring.

The current design keeps resource ceilings entirely at the operator/host level (`[docker.grants]` in `config.toml` or per-workspace), because resource limits are a property of the *machine running the container*, not the *role's toolchain*. The same role repo should run with a tight limit on a laptop and a generous one on a CI runner without editing the role. The role manifest's `[docker]` table deliberately exposes only `min_profile`, `dind`, `allowed_hosts`, and `capabilities_add` — no resource fields. Grants can only raise a dimension above the active profile's default, never lower it, so manifest inheritance does not apply without a manifest-level tier.

If a future need for role-declared resource floors emerges (e.g. "this role always needs at least 8 GiB regardless of operator config"), this page is the starting point for that design — the operator-level grant model would need a `min_*` counterpart symmetric to `min_profile`, not a full parallel schema.

## Limitations and open questions [#limitations-and-open-questions]

* **Should `cpus` accept percentages explicitly?** `"3.0"` is unambiguous for Docker's own `--cpus` flag. multicode's `"300%"` form is arguably more readable but maps awkwardly onto Docker, which doesn't accept a percent sign on `--cpus`. jackin❯'s shipped `[docker.grants].cpus` is a plain `f64` (Docker-shaped), so this question is moot for the current design — it would resurface only if a backend-neutral layer were added later.
* **Should resource limits be inheritable across roles?** If a base role declares a limit and a derived role extends it, does the derived role inherit? This was flagged as a separate, larger role-inheritance design question, out of scope for V1, and remains unresolved because role-level resource limits were never adopted in the first place.
* **`size`-crate-style parsing.** multicode uses the `size` crate for binary/decimal size strings (`"12 GB"` vs `"16 GiB"`). jackin❯'s shipped grants parse size strings via a small in-repo parser (`parse_memory_bytes` in <RepoFile path="crates/jackin-runtime/src/runtime/docker_profile.rs">crates/jackin-runtime/src/runtime/docker\_profile.rs</RepoFile>) rather than pulling the dependency, since the supported format (a small set of binary/decimal size suffixes) is narrow enough that a hand-rolled parser stays simpler than wiring in a crate.
