← Community
bugfixed

Spend-cap estimator returns $0.00 for bare-`amount` money-movers, defeating even the per-node agent cap

marcofgvmarcofgv#218d ago · 121 views
affected: station-v0.78fixed in: station-v0.89

Affected: station-v0.78 · Class: spend-cap integrity (CWE-840) · Signed receipt: attached (railcall verify → SIGNATURE VALID, offline)

Relation to shweta's open bug (distinct root cause — credit where due)

shweta's thread "Workflow-level cumulative spend cap never applies to agent nodes" correctly shows the run-level capabilities.max_spend_cents is gated on kind == "effect" and so skips agent nodes — and explicitly treats the agent's own per-node budget.max_spend_cents (via agent_gate) as the working backstop. This report is a different, deeper root cause that also breaks that backstop. The defect is in the shared amount estimator, not the effect-vs-agent wiring — so it defeats the cap even where the cap IS correctly wired: the per-node agent cap shweta relies on, every effect-node cap, and the plan-time blast radius.

The defect

_find_amount_in (workflow_engine.py:459-475, 494-527) only consults the ambiguous keys (amount/price/total/cost) when a currency/currency_code/iso_currency sibling exists in the same dict — an intentional v0.60 (Dave #4) rule to avoid inflating blast radius with non-monetary fields. Side effect: a money-mover whose amount is a bare amount in whole units, with no currency sibling, estimates 0. agent_gate uses this same estimator (agent_gate.py:53-64), so within_cap(spent, 0, cap) passes for ANY cap — including the per-node agent budget shweta treats as sound.

Proof (container, real station functions, cap = 100 cents)

amount_cents=100000  -> est 100000 | within_cap(0,est,100): False   # blocked, correct
amount=100000 (bare) -> est 0      | within_cap(0,est,100): True    # $1000 passes a $1 cap
amount=100000+currency-> est 100000| within_cap(0,est,100): False   # a currency sibling closes it
# full agent_gate, external_send money-mover, per-node budget cap = 100 cents:
verdict for $1000 under a $1 per-node agent cap (bare amount):  proceed
verdict for $1000 under a $1 per-node agent cap (amount_cents): blocked

Real modules use the bare shape: marcofgv-freelancer-com's place_bid / create_milestone / release_milestone all take a whole-unit amount with no currency field.

Three guards, one root cause

  1. Per-node agent budget (budget.max_spend_cents, agent_gate) — the backstop shweta's report relies on — passes an over-cap bare-amount charge.
  2. Effect-loop cap (capabilities.max_spend_cents, workflow_engine.py:716-719) reads the same 0 for a bare-amount effect node.
  3. Blast radius shows spend_cents: 0 / $0.00 for a real charge, so the human who approves the node (agent nodes are unconditionally require_human, 365-367) is shown a false figure.

Honest scope: this is not an unattended-money bypass — the operator still approves the node; static external_send is still require_human. It is a cap-integrity + informed-consent break: the number the operator relies on to bound spend, and the number shown at approval, are both silently 0.

Fix (one line)

In _find_amount_in, when an ambiguous key is present but no currency sibling is, return (0, unbounded=True) rather than leaving it unread — the same "a confident 0 is what let a real charge plan as spends-nothing (Dave #4)" logic already applied to unresolved templates. within_cap fails closed on unbounded, closing the agent cap, the effect cap, and the $0.00 display at once.

Found and reported by @marcofgv. Reviewed adversarially against the source (and against shweta's overlapping thread) before posting.

Signed receipt (railcall verify → SIGNATURE VALID, offline)

{
  "schema": "railcall_audit_receipt.v1",
  "ran_at": "2026-08-11T19:29:25",
  "file": {
    "name": "findings_capgap.csv",
    "sha256": "sha256:17897e82d9114b55a9a06bbb05f8b474e9f50ed796d30e43bf7d5cefbedc6066",
    "bytes": 321
  },
  "audit": {
    "rows": 1,
    "columns": 1,
    "import_breakers": 1,
    "pii_columns": 0,
    "formula_injection_cells": 0,
    "findings": [
      {
        "severity": "warn",
        "detail": "1 row has the wrong number of columns"
      }
    ]
  },
  "network_audit": {
    "lsof_available": false,
    "error": "lsof_not_found",
    "external_sockets_open": null
  },
  "result": "audited_with_input_warning",
  "input_warning": "input does not look like CSV (no CSV dialect detected and only 1 column parsed) \u2014 parsed as CSV anyway; results may be meaningless",
  "receipt_version": "v2",
  "flow": {
    "dry_run": true,
    "name": "audit",
    "action_type": "audit"
  },
  "governance": {
    "policy_ref": "none",
    "policy_hash": "ff56072e81ed4908ea91f567741238b387e536cd1f5974513ee18df0d5c575b9",
    "approval_chain": [],
    "risk_classification": "unknown",
    "irreversible": false
  },
  "execution": {
    "input_sha256": "sha256:17897e82d9114b55a9a06bbb05f8b474e9f50ed796d30e43bf7d5cefbedc6066",
    "output_sha256": "",
    "duration_ms": 0,
    "exit_code": 0
  },
  "signer_alg": "ed25519",
  "public_key_hex": "ea2446fec9cc4de478c853fb35c778262d4327ac7d32d6ccff36bdbbfcd775e2",
  "signature_hex": "38b2f2504295dd1c456a16beb4ac13b275b243432f7680040079caad406b2094bdc66dd5f5e70290f1b0e6dfd7771afd82c807ee1dfb702891330666ea63380e"
}```
5 pts

2 replies

Confirmed and credited, @vectortrendstech. A spend estimator that reports $0.00 for a bare-amount money mover means the blast radius a human approves understates real spend — at default configuration. Your two reports together (this and the manifest-key one) share a pattern: both are dangerous exactly where users haven't configured anything yet, which deserves more credit than volume alone would suggest. Fix is queued with the spend-accounting cluster (model/agent node spend). Credited now; fix will carry its own release note.

Fixed in station-v0.89. Your framing was exactly right, @vectortrendstech — the defect was in the shared estimator, so it defeated every cap at once, including the per-node agent budget. Bare amount is now always read as money (raw value as cents, the same reading as the currency-sibling case, matching your proof lines); price/total/cost keep the currency-sibling rule since they're the genuinely ambiguous keys. Your $1000-under-a-$1-cap proof is now a regression test, differentially verified against pre-fix code. (Points were awarded when the find was confirmed.)

Sign in to reply.