Skip to content

SpendControl: a command surface for setPolicy/clearPolicy — hand-editing spending.json is the only operator path #301

Description

@twzrd-sol

Follow-up to the scope note on #268. On main (fa0b1468, v0.12.263) spend policy is set only through the library — SpendControl.setLimit/clearLimit/setPolicy/clearPolicy, src/spend-control.ts:337–370. Nothing in src/cli.ts (unchanged since 07a8a384) or the plugin commands registered in src/index.ts:1979–2228 calls them. The operator path is hand-editing spending.json and restarting, because limits load once in the constructor (spend-control.ts:331–335) and are never re-read.

Proposal — one handler behind clawrouter policy … and /policy …, shaped like /exclude add|remove|clear (src/commands/exclude.ts, index.ts:2228):

policy                              # print limits + lists on disk
policy set|add|remove <list> <v>…   # <list> = exact PolicyList name
policy clear <list>
policy limit <window> <usd>|clear   # <window> = exact SpendWindow

Fail closed on input; on any refusal exit non-zero and write nothing:

  • allowedNetworks values must be CAIP-2. base/solana refuse, and the message names CAIP2_BASE/CAIP2_SOLANA_MAINNET. No nickname translation.
  • 0x-prefixed values that are not 40 hex refuse — a typo'd denylist entry is fail-open.
  • <usd> parsed by /^\d+(\.\d+)?$/, never parseFloat ("5abc" → 5).
  • remove that empties a list routes to clearPolicy: setPolicy rejects [] (:356) while load treats it as absent (:209).
  • Malformed file (policyFileBroken, :327): save() returns silently (:681), so a command built on setPolicy would report success and write nothing. Expose that state — getStatus() (:641) does not — and exit non-zero naming the field.

Live proxy: re-read limits at check() entry when the file mtime changes, or have the command say a restart is required. "Applied" with neither is the worst outcome.

Tests: extend src/proxy.spend-policy.test.ts — inject spendControl, run the handler against the same storage after startProxy, assert signerCalls === 0 for the newly blocked payee; deleting the reload path must fail it. Unit: nickname refused with the file byte-identical; malformed file → non-zero exit, file untouched.

PR to follow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions