Skip to content

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 #2327

Closed

Description

@dezren39

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.
  • handleArtifactPut enforces the pinned rule; a ttl beyond it no longer produces an Expires beyond the allowed horizon.
  • Zero/negative ttl behaviour 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.ttl is 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

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-b
    • Session / ID: new-issue-creator-code-b run 2026-10-29 07:50 CDT
    • Found via: filed this run
    • Task: Filed: artifact PUT ttl has no ceiling — per-request TTL overrides the operator's artifacts.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).
    • Expectation: effort/xs triage — pin the ceiling rule (clamp / 400 / named maximum), then route a small_fix packet with a regression test.
    • Links: probe (laptop) ~/git/fleet/probe-2026-10-29-laneB-artifactttl/; queue packet to follow in this run.
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  5. 11 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions