Repository navigation
Description
mcpx(daemon): artifact PUT ttl has no ceiling — a per-request TTL overrides the operator's artifacts.ttl (24h) up to ~292 years, so GC never collects the artifact
Evidence
pkgs/mcpx/internal/daemon/routes_exec.go:371-379 (main 5a34fa70), handleArtifactPut (POST /v1/artifacts, routes_exec.go:38):
if ttl := q.Get("ttl"); ttl != "" {
d, perr := time.ParseDuration(ttl)
if perr != nil {
writeErr(w, http.StatusBadRequest, fmt.Errorf("ttl %q: %w", ttl, perr))
return
}
put.TTL = d
}Any duration time.ParseDuration accepts is taken verbatim. There is no ceiling, and no comparison against the operator's configured retention.
The store then applies it verbatim — pkgs/mcpx/internal/artifacts/artifacts.go:303-312 (register):
ttl := p.TTL
if ttl <= 0 {
ttl = s.opts.TTL
}
...
Expires: now.Add(ttl),and GC is the only expiry path — artifacts.go:515 (GC): SELECT id FROM artifacts WHERE expires <= ?. An expiry centuries out is, for GC, never.
The operator's retention setting this overrides: artifacts.ttl, default 24h (pkgs/mcpx/docs/configuration.md:257; the store default is wired from the settings registry at routes_exec.go:93 / internal/cli/exec.go:163).
Probe (laptop, real artifacts.Store, main-identical files)
Probe worktree on the laptop: ~/git/fleet/probe-2026-10-29-laneB-artifactttl/ (relevant files md5-identical to sandbox main 5a34fa70: artifacts.go 0022b738…, routes_exec.go de9f0870…). A test in internal/artifacts opened a store with the operator default TTL: 24h and called the real Store.Put with per-put TTLs:
| per-put TTL | result |
|---|---|
0 (control) |
expires +24h (operator default) |
-1h (control) |
expires +24h (store's ttl <= 0 fallback) |
876000h (100 years) |
accepted — created 2026-10-29, expires 2126-10-05 |
2500000h (~285 years) |
accepted — expires 2312-01-10 |
Parse-step controls: time.ParseDuration("876000h") and ("2500000h") succeed; only a string overflowing int64 nanoseconds (~292.47 years, e.g. a 20-digit hour count) errors, and the handler correctly 400s on that parse error. So the effective ceiling today is Go's int64-nanosecond limit, not any retention policy.
Why it matters
artifacts.ttl is the operator's retention promise: how long a registered file is kept before it is collected (configuration.md:257). The per-request ?ttl= parameter silently repeals it for that artifact, with no bound and no log line. Any script or client that can reach the daemon — artifact registration is the normal artifact() script path, per the handler's own comment — can pin blobs effectively forever: a typo'd unit (ttl=876000h for 876000s-ish intent), a careless "keep it a long time" value, or simple accumulation of large-TTL artifacts defeats GC and grows the blob store until the store quota (a hard ceiling checked at put time, not a retention mechanism) starts refusing new artifacts for everyone. The failure surfaces months later as disk/quota pressure, with nothing pointing back at the TTL that caused it.
This is the same shape as #2231 (per-request waitMs overriding operator wait settings with no cap), on a different surface: there the cost was a pinned handler, here it is pinned storage.
Acceptance criteria
- Triage pins the ceiling rule and records it on this issue: e.g. clamp per-request TTL to at most the operator's
artifacts.ttl, reject above it with 400, or a named absolute maximum — including what a legitimate long-retention caller should do instead. -
handleArtifactPutenforces the pinned rule; attlbeyond it no longer produces anExpiresbeyond the allowed horizon. - Zero/negative
ttlbehaviour is unchanged (store default applies), and ordinary TTLs at or under the ceiling work exactly as before. - A regression test covers an over-ceiling
ttl(e.g.876000h) at the handler or store boundary, plus the happy-path and default controls. - The effective rule is documented where
artifacts.ttlis documented (docs/configuration.md/ exec docs), so the per-request override's bound is discoverable.
Dedup
Searched open + closed issues for "artifact TTL", "artifacts ttl", and "ttl" generally (2026-10-29): #1884 is a TTL-renewal convention proposal (touch-on-read), #1653 is MCP task TTL clocked from creation — neither covers the artifact PUT ceiling. #2231 is the waitMs long-poll cap, a different endpoint and store; cross-linked as the same pattern, not a duplicate.
Activity
developing-today commentedon Oct 29, 2026
CODE-1176
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
ttlhas no ceiling — per-request TTL overrides the operator'sartifacts.ttl(24h) up to Go's ~292-year ParseDuration limit; store applies it verbatim and GC (expires <= now) never collects it. Verified with the real artifacts.Store on the laptop (876000h → expires 2126; 2500000h → expires 2312; 0/-1h controls → 24h default).11 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · parked
dezren39 commentedon Oct 31, 2026
agent-note:v1 — task 4329 resumed by lane B worker: PR #3094 @ 6e0b24b revalidated fresh — Codex review completed with no findings; laptop tests re-run first-hand this shift: go test ./internal/artifacts/ ./internal/daemon/ both pass, including TestHandleArtifactPutTTLCeiling (over-ceiling 400 naming both values, nothing stored). CI checks remain queued in the fleet runner backlog (none failed). Merging under the standing merge directive on the verified change.
fix(mcpx/daemon): cap artifact PUT per-request ttl at artifacts.ttl (#…