Skip to content

mcpx(settings): Clamp validates a dial request against the ceiling's enum, not the dial's — prompt.autonomy accepts apply/off/advise/ask #1641

Closed

Description

@dezren39

Summary

settings.Set.Clamp validates a requested dial value against the ceiling's enum, not the dial's own enum. For the one dial whose enum is a strict subset of its ceiling — prompt.autonomy ({propose, run}, clamped by autonomy.max's full ladder {off, advise, ask, propose, apply, run}) — a request for a value the dial can never hold passes through untouched, unlowered and unerrored, as long as it is anywhere on the ceiling's ladder and at or below the ceiling. The error for a truly unknown value also lists the ceiling's values, telling the caller that apply/off/ask are acceptable for prompt.autonomy.

Evidence (verified on main ed8e5910)

pkgs/mcpx/internal/settings/clamp.go:36-48 (Set.Clamp): the only rank check is

want, ok := cdecl.Rank(requested)   // cdecl = the CEILING's declaration (autonomy.max)
if !ok {
    return Clamp{}, fmt.Errorf("%s: %q is not one of %v", path, requested, cdecl.Enum)
}

decl.Rank(requested) (the dial's own enum) is never consulted. The declarations (internal/settings/consumer.go:118-119 and :156-157): autonomy.max has Enum: AutonomyLevels (off, advise, ask, propose, apply, run, consumer.go:12); prompt.autonomy has Enum: []string{"propose", "run"}, ClampedBy: "autonomy.max". checkClamp (clamp.go:82-115) deliberately allows this shape — a dial's values must be an ordered subset of the ceiling's — so subset dials are a designed-for case, not a misconfiguration.

Probe on the laptop driving the real internal/settings package with the real registry (default autonomy.max = run; probe + log in the fleet's run records, run twice with identical output):

Clamp(prompt.autonomy, "propose") -> value="propose" lowered=false err=nil     (control, valid)
Clamp(prompt.autonomy, "run")     -> value="run"     lowered=false err=nil     (control, valid)
Clamp(prompt.autonomy, "apply")   -> value="apply"   lowered=false err=nil     (BUG: not a prompt.autonomy value)
Clamp(prompt.autonomy, "off")     -> value="off"     lowered=false err=nil     (BUG: same)
Clamp(prompt.autonomy, "advise")  -> value="advise"  lowered=false err=nil     (BUG: same)
Clamp(prompt.autonomy, "ask")     -> value="ask"     lowered=false err=nil     (BUG: same)
Clamp(prompt.autonomy, "plan")    -> err=prompt.autonomy: "plan" is not one of [off advise ask propose apply run]  (error lists the CEILING's enum)
Clamp(repair.autonomy, "apply")   -> value="apply" err=nil                     (control: repair's enum IS the full ladder)
Clamp(repair.autonomy, "plan")    -> err=... "plan" is not one of [off advise ask propose apply run]  (correct list here)

Existing coverage misses exactly this: internal/settings/clamp_test.go builds the same subset shape (t.dial = {propose, run} under the full ladder) but only ever clamps run, propose, and plan — a value outside both enums. No ceiling-enum-but-not-dial-enum request (apply/off/advise/ask) is tested.

Reachability today (honest scope)

The two prompt routes are not currently exploitable end-to-end: promptAutonomy (internal/daemon/routes_consumer.go:507-518) pre-validates the request body to propose/run before calling Clamp, and the call-settings header path (callSettings, routes_settings.go:79 / Normalize) and file/env layers validate against the dial's own enum. What is broken is Clamp itself — the function its own doc names as the choke point for this job ("A handler that takes the dial from a request body calls this", clamp.go:23-26). Any present or future handler that relies on Clamp alone (as its contract invites) gets out-of-enum values back labelled as the request's autonomy: e.g. apply satisfies AutonomyAtLeast(level, "propose") (routes_consumer.go:337,:400) yet is neither run nor a handled level, so a recipe/intent resolution would return its source unexecuted under a nonsense autonomy: "apply" label, with no error anywhere.

What must NOT change

Lowering to a ceiling value below the dial's whole range is intentional and pinned by the existing test (clamp_test.go: ceiling off lowers a propose request to off, and routes_consumer.go's belowPropose exists for exactly those levels). The defect is only the unlowered pass-through of a requested value that is not in the dial's enum (and the wrong enum in the error). A fix that rejects lowered-to-ceiling results would break that design.

Acceptance criteria

  • Clamp("prompt.autonomy", x) for x in {apply, off, advise, ask} (in the ceiling's enum, not the dial's) with ceiling run returns an error instead of passing x through as the value — or, if triage prefers a different ruling, that ruling is pinned by tests and documented on Clamp.
  • The error for an invalid request names the dial's permitted values (propose, run for prompt.autonomy), not the ceiling's ladder.
  • Valid requests are unchanged: propose/run pass at or below the ceiling; a request above the ceiling is lowered to the ceiling value (including a ceiling below the dial's range, e.g. ceiling off lowering propose → off, per the existing pinned behaviour).
  • repair.autonomy / hooks.autonomy (dial enum == ceiling enum) behave exactly as before.
  • New tests in internal/settings cover the ceiling-enum-but-not-dial-enum shapes against a subset dial (the clamp_test.go fixture shape already supports this).

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    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
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a cron run, 2026-10-28 04:35 CDT
    • Found via: filed this run
    • Task: Triage the Clamp dial-enum validation gap: Set.Clamp ranks a requested dial value only in the ceiling's enum, so ceiling-enum values outside prompt.autonomy's own enum (apply/off/advise/ask) pass through unlowered and unerrored, and the invalid-value error lists the ceiling's ladder instead of the dial's values.
    • Expectation: effort/s — pin the ruling (reject unlowered out-of-dial requests, error naming the dial's enum; preserve the pinned lowering-to-ceiling behaviour), fix Clamp, add subset-dial regression tests.
    • Links: evidence probe in the fleet run records (probe test in the real internal/settings package on the laptop, run twice identical); queue packet enqueued 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. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (triage)

    • Agent: queue worker (repo-worker-code), lane B shift
    • Session / ID: queue task 1781
    • Found via: queue claim
    • Task: Triage of the Clamp dial-enum validation bug — verdict: CONFIRMED, exactly as filed.
    • Findings:
      • Re-verified verbatim on current main b5d334b0b6de (2026-10-31): pkgs/mcpx/internal/settings/clamp.go Set.Clamp computes the only rank check as cdecl.Rank(requested) where cdecl is the ceiling's declaration; the error message formats cdecl.Enum; the dial's own decl.Rank is never consulted. The code path is unchanged from the packet's ed8e5910 evidence.
      • Probe evidence stands (real internal/settings package, run twice identical, probe-2026-10-28-laneA-clamp/out.log): Clamp(prompt.autonomy, apply|off|advise|ask) all return the value with lowered=false, err=nil — none is a prompt.autonomy value ({propose, run}); plan errors but lists the ceiling's ladder as the permitted values. Controls propose/run pass; repair.autonomy (enum == ladder) behaves correctly; ceiling=ask lowers apply/propose → ask (the pinned below-range lowering the fix must preserve).
      • No PR addresses this (fresh search 2026-10-31); the masking routes noted in the packet (promptAutonomy pre-validation, Normalize) do not change the verdict — Clamp remains the documented choke point for request-body dials.
      • Baseline caveat for the fixer: internal/settings tests are red on clean main for unrelated reasons filed as mcpx: internal/settings tests red on clean main — TESTING.md missing from compendium manifest, model.* settings lack effect probes, env doc drift #2846 — baseline-compare rather than inheriting or fixing them.
    • Outcome: Fix follow-up enqueued as queue task 4071 (small_fix, packet packet-1641-fix.json): validate the requested value against the dial's own enum first (error naming the dial's values), then rank in the ceiling for lowering; preserve pinned lowering semantics; regression tests on the existing subset-dial fixture in clamp_test.go.
    • Links: follow-up task 4071 · mcpx: internal/settings tests red on clean main — TESTING.md missing from compendium manifest, model.* settings lack effect probes, env doc drift #2846 (baseline reds)
  7. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Oct 31, 2026
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Fix PR opened: #2883

    Branch: queue/4071-1641-clamp-dial-enum → PR #2883 (open, not merged — checks below).

    Change (pkgs/mcpx/internal/settings/clamp.go, +3): Set.Clamp now validates requested against the dial's own enum first (decl.Rank, error names the dial's values), then ranks in the ceiling for the lowering comparison. The ceiling check stays as a defensive backstop.

    Tests:

    CI on #2883: basic: lint pass, plugin tests pass, nix build .#mcpx-docs pass — but stress: PR changed tests fails solely on those same 5 pre-existing #2846 tests (verified: the job log contains only those 5 test names, 10x each under race/shuffle/repeat; no clamp failure). Per standing merge criteria (checks green) and the packet's out-of-scope ruling on #2846, not merging — merge when #2846 clears or by explicit override.

    🤖 Agent note · pr_opened, merge-blocked on pre-existing reds

  9. added
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  10. added a commit that references this issue on Nov 2, 2026
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/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions