Skip to content

feat(spec)!: drop Persistence/KitDir, add locked/resources, list mounts#37

Open
mdelapenya wants to merge 6 commits into
mainfrom
kit-spec-changes
Open

feat(spec)!: drop Persistence/KitDir, add locked/resources, list mounts#37
mdelapenya wants to merge 6 commits into
mainfrom
kit-spec-changes

Conversation

@mdelapenya
Copy link
Copy Markdown
Member

What's changed

Five field-scoped, breaking-but-experimental changes to the kit spec, each a separate commit:

  • Remove Manifest.Persistence — declared on the manifest, parsed, inherited, displayed, but no runtime decision consumed it. Volumes are already explicit via Manifest.Volumes.
  • Remove Manifest.KitDir — same shape as Persistence: stored, copied during inheritance, rendered by inspect commands, but never consulted at runtime. No kit declares it.
  • Add top-level locked — a list of dotted YAML paths that child kits must not override during extends: resolution. The spec library only validates well-formedness; enforcement is the merge consumer's job.
  • Add agent.resources — optional container resource constraints (cpu as float64 cores, memoryMB as int64, gpu as a consumer-defined string).
  • Switch volumes/tmpfs to list form with a shared MountSpec — both Manifest.Volumes and Manifest.Tmpfs move from map[string]string (with packed option strings) to []MountSpec{Path, Size, Mode}. The two fields share one entry type since their per-entry shape is identical; the semantic distinction lives on the slice field.

TCK gains pure-logic sub-tests for locked and resources, and iterates the slice form for volumes and tmpfs. Agent ttl is intentionally deferred — there are unresolved comments on the proposal that need to land first.

Why is this important?

The kit spec accumulated fields that looked configurable but had no effect — Persistence and KitDir were declared and validated but never read. Removing them clears the kit-author surface and avoids the fiction that they do something.

The two additive fields come from the next phase of kit-author requests: locked gives parent kits a way to protect critical fields from being overridden, and resources declares container limits that the engine will wire to the runtime (matters most for cloud workloads, where CPU/memory/GPU constraints aren't always inherited from the host).

The mount restructure replaces a flat map whose values had to pack options into a single string ("size=512m,mode=1777") — awkward to validate, awkward to extend — with a discrete list-of-struct form. Volumes and tmpfs share MountSpec because their fields are identical; the validators delegate to one shared validateMounts helper that supplies the field name as the error prefix, so failures still read volumes[i] or tmpfs[i].

Kits are documented as experimental, so the removals and shape change don't require a schema bump.

Changes

spec/

  • Drop PersistenceEphemeral/PersistencePersistent constants, Manifest.Persistence, agentBlock.Persistence, the flat-field rejection in normalize, and the persistence rule in ValidateManifest.
  • Drop Manifest.KitDir.
  • Add Artifact.Locked []string and specFile.Locked parsing; add ValidateLocked (rejects empty entries, malformed dotted paths, duplicates).
  • Add Resources type, Manifest.Resources, agentBlock.Resources, normalize wiring, and non-negative CPU/memory validation in ValidateManifest.
  • Introduce MountSpec{Path, Size, Mode} used by both Manifest.Volumes and Manifest.Tmpfs. Collapse ValidateVolumes/ValidateTmpfs into thin wrappers around a shared validateMounts(field, mounts) helper.

tck/

  • Drop the persistence sub-test from RunValidationTests.
  • Add a locked sub-test that mirrors spec.ValidateLocked.
  • Add a resources sub-test asserting CPU and MemoryMB are non-negative.
  • Volumes/tmpfs pure-logic sub-tests iterate the slice form.
  • deriveTmpfs takes []spec.MountSpec and composes the comma-separated option string Docker expects from Size/Mode, preserving the existing runtime assertTmpfs check unchanged.

Pure-logic only — runtime container assertions for resources (applying limits via testcontainers and inspecting HostConfig) were considered but kept out of scope. Resource enforcement is the engine's responsibility, and the TCK should not pretend to be the engine.

Kits

Eight kit specs drop their no-op persistence: persistent lines: amp, claude-ollama, nanobot, nanoclaw, openclaw, opencode-model-runner, pi, trivy. No contrib kit declared volumes: or tmpfs: in yaml, so kit specs are otherwise unchanged.

Migration

Downstream consumers of this library will need to:

  1. Bump the dep to pick up the new types.
  2. Drop their own dead references to Manifest.Persistence and Manifest.KitDir.
  3. Adapt their volume and tmpfs customizers from map[string]string to []spec.MountSpec. The packed option string ("size=10g") is now discrete fields; the consumer composes whatever runtime form Docker expects.
  4. Wire agent.resources to the container runtime when ready to enforce limits.
  5. Wire locked into the inheritance merge step to actually reject child overrides.

Test plan

  • go test ./... passes across all 15 packages in this repo (spec, tck, and every kit's integration tests).
  • All five commits are GPG-signed and DCO signed-off.
  • Downstream consumer adapter PRs follow separately.

The field was stored, validated, inherited, displayed, and tested,
but no engine path actually consumed it; volumes are already
declared explicitly via Manifest.Volumes. Drop:

- Persistence constants and Manifest.Persistence
- agentBlock.Persistence (and the flat-field rejection)
- ValidateManifest persistence rule
- TCK suite persistence sub-test

Kits in this repo that declared `persistence: persistent` no
longer carry the field, since it was no-op all along.

Kits are documented as experimental, so no schema bump.

Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
Same shape as the Persistence removal: KitDir was declared on
the manifest, parsed from yaml, copied during inheritance, and
rendered by `sbx kit inspect`, but no runtime path consumed it
to make a decision. No kit in this repo nor in the engine's
embedded agents declares it.

Drop the field outright. The engine has its own dead-code
references (inherit copy + CLI display) to clean up downstream.

Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
Kits can now declare a list of dotted YAML paths that child
kits must not override during single-parent inheritance:

    locked:
      - agent.image
      - network.allowedDomains

The spec library only validates well-formedness — empty entries,
malformed paths (leading/trailing/double dots, disallowed chars),
and duplicates are rejected. Whether a given path is meaningful
against the canonical Artifact shape is the consumer's call;
the engine's inherit step is the natural enforcement site.

TCK suite gains a `locked` sub-test that mirrors ValidateLocked,
so kits exercising the field get a visible per-kit pass/fail
alongside the other manifest sub-tests.

This is the surface; the merge-time check follows downstream.

Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
Optional container resource limits declared under the agent block:

    agent:
      image: ...
      resources:
        cpu: 4
        memoryMB: 8192
        gpu: "1"

All three fields are optional; unset means "no constraint from
the spec". CPU is float64 (fractional cores allowed); MemoryMB
is int64 mebibytes; GPU is a string the consumer interprets
(e.g. "1", "all", a vendor selector).

Resources normalizes onto the Manifest alongside Template and
AIFilename. Validation rejects negative CPU/memory; the TCK
suite gains a `resources` sub-test so kits exercising the field
get a visible per-kit pass/fail alongside the other manifest
sub-tests.

Agent TTL is intentionally deferred — there are unresolved
comments in the proposal doc that need to land first.

Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
Replace Manifest.Volumes and Manifest.Tmpfs (both map[string]string
where the value held packed options like "size=10g,mode=1777") with
[]MountSpec, borrowed from the dash kit format. Discrete fields are
easier to validate, easier to extend, and read better in yaml:

    volumes:
      - path: /home/agent/.cache
      - path: /tmp/scratch
        size: 100m
        mode: 1777
    tmpfs:
      - path: /tmp/agent-scratch
        size: 512m
        mode: 1777

Volumes and tmpfs share the MountSpec type (Path, Size, Mode) because
their per-entry shape is identical; the semantic distinction lives on
the Manifest field that holds the slice. ValidateVolumes and
ValidateTmpfs collapse into thin wrappers around a single
validateMounts helper that takes the field name as the error prefix,
so failures still read "volumes[i]" or "tmpfs[i]".

TCK pure-logic sub-tests iterate the slice form; tck/derive.go's
deriveTmpfs composes the comma-separated option string Docker
expects from Size/Mode, preserving the existing runtime assertTmpfs
check unchanged.

No contrib kit declared `volumes:` or `tmpfs:` in yaml, so kit
specs are unchanged. The engine consumers (volumeCustomizer and
tmpfsCustomizer) need to be adapted downstream.

Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
@mdelapenya mdelapenya requested a review from a team as a code owner May 13, 2026 20:47
ValidateCommandsPolicy already rejects empty/relative initFile
paths and unsupported content placeholders; extend it to also
reject malformed Mode strings using the same octalModePattern
already defined for MountSpec.

The engine's kit/validate.go has been carrying this check as a
local extension. Pushing it upstream means the spec library is
the single source of truth and the engine can drop its wrapper.

Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant