Skip to content

sec(ladder): LadderExecutionEnabled follows #1765's config-driven-scheduled-execution pattern (currently inert) #178

Closed

Description

@cristim

Summary

Found while investigating LeanerCloud/cloud-commitments-cli#1765 (config-driven scheduled auto-exchange bypasses the execute:ri-exchange carve-out). The commitment-laddering feature has the identical pattern -- a config write gated only on update:config controls whether a scheduled task's write side is armed with real cloud-provider clients -- but it is not exploitable today because the planning engine never actually calls the write-side methods. This issue exists so the gap is discoverable by whoever wires that engine, rather than only living as a comment on a different issue's thread.

The pattern

GlobalConfig.LadderingEnabled + LadderExecutionEnabled (internal/config/types.go:67-80) are both writable only via update:config (PUT /api/config -> updateConfig, internal/api/handler_config.go:61). Both are threaded into the scheduled TaskLadderRun task:

handleLadderRun (internal/server/handler_ladder.go:83)
  -> runLadderConfigs -> processOneLadderConfig
  -> buildAndWireCapability(ctx, region, accountID, executionEnabled)   (handler_ladder.go:254)
  -> wireLadderWriteSide(ctx, executionEnabled, ...)                    (internal/server/ladder_write.go:116)

When executionEnabled (== globalCfg.LadderExecutionEnabled) is true, wireLadderWriteSide calls awsladder.WireWriteSide, which wires real EC2 and Savings Plans clients plus an exchangeRunnerAdapter that can trigger RI-exchange auto-execution (the same exchange.RunAutoExchange at the center of LeanerCloud/cloud-commitments-cli#1765) onto the LadderCapability. This is exactly LeanerCloud/cloud-commitments-cli#1765's shape: a config write gated only on update:config controls whether a scheduled task can act with real money-moving authority, with no execute:purchases/Purchaser-group check anywhere on the config-write path.

Why it is not exploitable today

handleLadderRun's own docstring states it plainly: "plan-only: no PurchaseLayer, ReshapeBuffer, email, or approval tokens" (internal/server/handler_ladder.go:82). Confirmed by grep: there are zero non-test callers of .PurchaseLayer( or .ReshapeBuffer( on the LadderCapability interface anywhere in the codebase. wireLadderWriteSide builds and threads the write-side wiring end-to-end, but nothing in the current planning engine invokes it -- LadderExecutionEnabled = true currently wires real AWS clients that are never called.

Why this needs its own issue instead of a note on LeanerCloud/cloud-commitments-cli#1765

Inert-today is exactly the state that stops being true the moment someone wires the ladder engine to actually call PurchaseLayer / ReshapeBuffer (the later phase of issue LeanerCloud/cloud-commitments-go#27). Whoever does that work needs to know this landmine exists before shipping it, and a comment buried in LeanerCloud/cloud-commitments-cli#1765's thread is invisible to someone searching the ladder code or working LeanerCloud/cloud-commitments-go#27. This issue is the thing that surfaces when they look.

Suggested fix direction

When the ladder engine is wired to actually execute (future work on LeanerCloud/cloud-commitments-go#27), apply the same fix LeanerCloud/cloud-commitments-cli#1765 recommends there: require execute:purchases (or Purchaser-group membership) specifically for the write that flips LadderExecutionEnabled (or LadderingEnabled + LadderExecutionEnabled together) to true, via a shared helper analogous to whatever LeanerCloud/cloud-commitments-cli#1765 lands, rather than relying on update:config alone. Do not block on this issue before LeanerCloud/cloud-commitments-cli#1765 -- LeanerCloud/cloud-commitments-cli#1765 is the live, exploitable instance; this one is preventive.

Refs LeanerCloud/cloud-commitments-cli#1765, LeanerCloud/cloud-commitments-cli#1758, LeanerCloud/cloud-commitments-cli#1644, LeanerCloud/cloud-commitments-go#27.

Activity

  1. cristim commented on Nov 14, 2026

    @cristim
    MemberAuthor

    Claimed by claude-platform-2 (coordinator assignment 2026-11-14; not touching store_postgres.go, plans.ts, recommendations.ts): premise-check against main, then implement and close (Closes #178) or post the evidence/blocker.

  2. cristim commented on Nov 14, 2026

    @cristim
    MemberAuthor

    Closing: the carve-out this issue asked for is on main. Turning LadderExecutionEnabled false->true needs a pre-resolved execute:purchases grant checked against the credential presented (an admin API key does not satisfy it) inside the UpdateGlobalConfigAtomic closure (internal/api/ladder_arming_grant.go, requireLadderExecutionSwitchGrant, landed in 72fa7e4 'require execute:purchases to arm ladder execution', eb313af, bab5f3d; before-state captured at internal/api/handler_config.go:106; every change stamped with who/how/when). Disarming or changing plain LadderingEnabled stays an update:config write. Arming or raising a ladder_config (auto-approve mode, higher cap) is gated by the same file (ladderConfigArmed, ladderCapRaised). The ri-exchange equivalent from cli#1765 is 00cfd32 (#1773). Tests pin it: internal/api/ladder_arming_grant_test.go (granted, refused, key-only, admin-only, disarm), global_config_arming_grants_test.go, arming_global_test.go, handler_config_arming_pool_integration_test.go, and the store tests store_postgres_ladder_arming_integration_test.go / store_postgres_ladder_disable_integration_test.go. Verified by reading main this turn; the carve-out is the existing execute:purchases verb, as the issue itself proposed, not a new ladder-specific verb.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions