Skip to content

fix(senior-unilp-manager): reject bare value-taking flags instead of silently coercing to 1/True - #1706

Closed
andrew1234-arch wants to merge 1 commit into
use-agent-os:mainfrom
andrew1234-arch:fix/issue-1705
Closed

andrew1234-arch wants to merge 1 commit into
use-agent-os:mainfrom
andrew1234-arch:fix/issue-1705

Conversation

@andrew1234-arch

Copy link
Copy Markdown
Contributor

Linked issue

Fixes #1705

Summary

unilp/fmt.py's parse_args() turns a bare --flag (no value, or
immediately followed by another --flag) into the boolean True — correct
for a real switch, wrong for anything else. Every optional value-taking flag
in lp_write.py, lp_read.py, and ratchet.py read its value with either
args.get(name) or DEFAULT or a bare args.get(name) passed straight into a
helper, with no guard against that sentinel. True or DEFAULT evaluates to
True (Python or returns the first truthy operand), so int(True)
silently becomes 1 — never the real default, never an error.

Traced and empirically reproduced 20 affected call sites across the three
scripts, not just the original 11 I found (slippage-bps ×5, max-tick-drift
×2, deadline-secs, expiration-days, gas-multiplier, max-attempts):

  • 11 sites silently substitute 1 for the real default (--slippage-bps,
    --max-tick-drift, --deadline-secs, --expiration-days,
    --gas-multiplier, --max-attempts).
  • 8 sites crash with a low-level error naming no field (--from,
    --signer-env ×3, --max-fee-per-gas, --rpc ×3) — e.g.
    TypeError: str expected, not bool inside os.environ.get, or
    AttributeError: 'bool' object has no attribute 'strip' inside URL
    validation. Confirmed empirically for each, not assumed from reading.
  • 1 site silently resolves to the literal string "True" (--owner in
    lp_read.py), deferring the failure ~30 lines downstream into
    checksum_address's "not a 20-byte address" error.

I traced every slippage-bps/max-tick-drift use to confirm direction: both
compute protective bounds (max-in for mint/increase, min-out for
decrease/burn), so this bug always tightens the bound rather than loosening
it — it fails toward reverts and confusing errors, not toward fund loss.
Still a real, previously-undetected correctness bug affecting a script that
broadcasts real transactions.

This is not hypothetical or newly-invented: the sibling skill
poolsdotfun-token-launcher — which vendors this exact fmt.py convention
verbatim (its own PR #299 description: "vendors the stdlib crypto/RPC layer
from senior-unilp-manager... Copied verbatim where possible to stay
diffable") — was already patched for precisely this bug class. Its
poolsfun/fmt.py adds opt_str/opt_int/opt_float with a comment stating
the exact hazard this PR fixes. senior-unilp-manager's own fmt.py never
received the same fix. lp_read.py's main() even hand-rolls a narrower,
one-off guard for its own --rpc case
(args.get("rpc") if isinstance(args.get("rpc"), str) else None) — proof
the codebase already knows about this hazard but never applied a fix
consistently across the three scripts.

No competing PR exists for this issue.

Fix

Backported opt_str/opt_int/opt_float from poolsfun/fmt.py into
unilp/fmt.py, byte-for-byte identical to the already-accepted
implementation — no new design surface. Rewired all 20 call sites across
lp_read.py, lp_write.py, and ratchet.py. Range bounds on the new
opt_int/opt_float calls (slippage-bps: 0–9999; deadline-secs: ≥60;
gas-multiplier: ≥1.0) match poolsfun/pools_write.py's own precedent for
the identical parameters exactly, rather than inventing new limits.
max-tick-drift/max-attempts/expiration-days get a minimum=1, matching
poolsfun's max-salt-attempts convention.

lp_read.py's pre-existing, already-safe --rpc guard is left untouched —
it fails safe today and changing it would be an unrequested behavior change
outside this bug's scope.

Tests

scripts/selftest.py (offline, no RPC, no network):

  • Added a _raises() helper mirroring poolsfun/selftest.py's own raises()
    — checks the exception message names the field, not just that something
    raised, since a regression could swap in a different opaque exception that
    happens to also be a ValueError.
  • 16 new assertions on opt_str/opt_int/opt_float directly: absent falls
    back to default, values parse correctly, whitespace trims, every bare-flag
    case is rejected with a "needs a value" message (not coerced), non-numeric
    values are rejected, and every bound (slippage-bps 0/9999,
    deadline-secs 60) is enforced.
  • 1 new assertion at a real call site (lp_read.resolve_owner({"owner": True}))
    proving the fix is correctly wired in, not just correct in isolation.
  • Baseline was 733 assertions; now 750 — confirmed the new checks execute
    (not skipped) by running against upstream/main before and after.

Commands run:
uv run ruff check <5 touched files>
→ All checks passed!
uv run mypy src/agentos --show-error-codes
→ Success: no issues found in 625 source files
python3 scripts/selftest.py
→ 750 assertions pass
uv run pytest tests/test_skill_bundled_url_scheme.py tests/test_skill_rpc_error_non_dict_payload.py tests/test_skills_default_prompt_contract.py tests/test_skills_script_paths.py tests/test_skills_third_party_notices.py tests/test_tools/test_skill_view_budget.py -q
→ 67 passed

No ruff format --check — every touched file is under skills/bundled/,
exempt from the formatter per repo convention.

Third-party origin: none — opt_str/opt_int/opt_float are backported
from this same repository's own poolsfun/fmt.py (PR #299, already merged).

Fixes the underlying issue for good, per the acceptance criteria named above:
every one of the 20 confirmed call sites now raises a clear,
field-naming error on a bare flag instead of silently substituting a wrong
value or crashing opaquely — matching the standard the sibling skill already
set.

@andreapn

Copy link
Copy Markdown
Contributor

Thanks for the PR and the thorough write-up on #1705. The issue has been fixed by #1860, which is now merged — it ports the same opt_* helpers and routes every value-taking flag in all three scripts through them (this PR left lp_read.py's --mode/--quote/--token/--ranges/--tick-lower/--tick-upper and lp_write.py's --recipient/--liquidity/--amount* raw) without introducing new range bounds, which triage scoped out ("no new flags"). Closing this one.

@andreapn andreapn closed this Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: senior-unilp-manager optional flags silently misparse a bare --flag as 1/True instead of erroring

2 participants