Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
223f793
feat(executors): surface orphaned LP positions to agents β€” tick-promp…
fengtality Aug 13, 2026
06adc22
fix(mcp): resolve_orphan without executor_id returns a required-input…
fengtality Aug 13, 2026
d8fa769
test(mcp): pin the resolve_orphan required-input contract
fengtality Aug 13, 2026
0772a61
feat(clmm): expose bin_count on get_pool_info
fengtality Aug 13, 2026
7e622a3
chore: bump hummingbot-api-client to 1.5.8 for bin_count
fengtality Aug 13, 2026
4f1a9ad
feat(clmm): add manage_clmm so agents can recover orphaned LP positions
fengtality Aug 17, 2026
a091669
fix(clmm): point every orphan warning at manage_clmm
fengtality Aug 17, 2026
dbc9ffc
chore(deps): lock hummingbot-api-client at 1.5.8
fengtality Aug 17, 2026
8bf2905
style: black + isort the CLMM/orphan changes
fengtality Aug 17, 2026
04d1701
chore(agents): pin adaptive_grid_trader to sonnet
fengtality Aug 17, 2026
f5f25cb
feat(agents): rename Smart-Money Flow agent to Derive Options Trader
fengtality Aug 11, 2026
4df460d
fix(agents): make the Derive Options Trader routines pass #203's gates
fengtality Aug 17, 2026
3d9f2ef
test(agents): teach the numeric-credentials stub the SEC-178 access p…
fengtality Aug 18, 2026
6404c9b
chore(deps): resolve the 1.5.8 pin from the client PR branch until it…
fengtality Aug 18, 2026
73221b4
fix(agents): close four holes the adversarial review found in orphan …
fengtality Aug 18, 2026
2ad6a2b
revert(agents): drop the SYSTEM_CLEANUP tick-prompt warning
fengtality Aug 18, 2026
56f2a27
fix(clmm): position_info lists the wallet β€” pool_address was a Gatewa…
fengtality Aug 18, 2026
cb138bf
chore(deps): bump the client lock to the contract-gap commit
fengtality Aug 18, 2026
cb0695b
fix(dex): align with hapi contracts; drop pool-scoped AMM swaps from MCP
fengtality Aug 19, 2026
f5d1d70
fix(agents): gate LP writes; register the swap tool; stop forcing 1% …
fengtality Aug 19, 2026
7d194ec
fix(dex): read the fields hapi actually sends; resolve networks dynam…
fengtality Aug 19, 2026
fb38b81
fix(agents): describe LP writes in the confirmation prompt
fengtality Aug 19, 2026
8e5a22f
chore(deps): lock the client to the dead-pool-method removal
fengtality Aug 19, 2026
0b22d96
test(lp): cover the last read action, swap get_status
fengtality Aug 20, 2026
ae410a0
docs(gateway): record GW-17, the add/remove sign split
fengtality Aug 20, 2026
42c5786
docs(gateway): confirm GW-17 with the completed round trip
fengtality Aug 20, 2026
8df9d52
docs(gateway): record GW-18, pancakeswap-sol's flattened close accoun…
fengtality Aug 20, 2026
528f1ee
test(lp): exercise orca CLMM and meteora DAMM v2; record GW-19
fengtality Aug 20, 2026
f7e210f
test(lp): size the meteora legs to the fill, and read balances with r…
fengtality Aug 20, 2026
f17b0da
docs(gateway): record GW-20, position rent booked as deposited liquidity
fengtality Aug 20, 2026
ed8a147
docs(gateway): record GW-21; rename the meteora close step to drain
fengtality Aug 20, 2026
4362d6a
docs(gateway): GW-21 fixed by collapsing the routes; restore the clos…
fengtality Aug 20, 2026
5c273bc
docs(gateway): GW-20 fixed in both halves
fengtality Aug 20, 2026
51300d7
fix(clmm): read the position that was asked for
fengtality Aug 20, 2026
87a4fad
docs(gateway): record GW-22 so a parallel session does not re-find it
fengtality Aug 20, 2026
d2a1dc0
docs(gateway): record GW-23, money typed as JSON number
fengtality Aug 20, 2026
2485e00
test(orca): cover all three range shapes, with ranges derived from spot
fengtality Aug 20, 2026
1529d04
docs(gateway): record GW-24, Orca's open spends the slippage bound
fengtality Aug 20, 2026
81f2c1d
docs(gateway): summarise what is fixed, spell out what is not
fengtality Aug 20, 2026
5f0dd5c
docs(gateway): record GW-25, narrow in-range closes fail on slippage
fengtality Aug 20, 2026
d3ba151
docs(gateway): GW-14 and GW-15 fixed, GW-16 in its concrete half
fengtality Aug 20, 2026
674d07e
chore: stop tracking GATEWAY_ISSUES.md
fengtality Aug 20, 2026
1ac95ff
fix(gateway): say what an add or a remove moved
fengtality Aug 20, 2026
086c010
docs(lp_executor): document the slippage ramp
fengtality Aug 20, 2026
6e62370
feat(swap): add the execute_quote action
fengtality Aug 20, 2026
167d27f
Merge remote-tracking branch 'origin/feat/ralph_improvemnts' into fea…
fengtality Aug 20, 2026
3d817d7
feat(swap): say what an approximated quote is, and add the two verifi…
fengtality Aug 20, 2026
14c8cdc
docs(order_executor): the amount haircut is conditional, not a constant
fengtality Aug 21, 2026
9ae4dba
fix(pools): default pool discovery to depth, and name the sort keys t…
fengtality Aug 21, 2026
8e268dc
docs(lp_range_config): the lp_rebalancer controller's fields are not …
fengtality Aug 21, 2026
7102046
docs(solana_dex_lp_expert): name the controllers the consult routing …
fengtality Aug 21, 2026
15eceff
chore: ignore every .env variant, not just the bare name
fengtality Aug 21, 2026
b6448ac
fix(ci): drop the live-mainnet scripts, and format what black flags
fengtality Aug 21, 2026
da98a23
Merge branch 'feat/ralph_improvemnts' into feat/lp-close-retry-ownership
fengtality Aug 21, 2026
7b54487
chore(agents): move this branch's agents/ work to a follow-up PR
fengtality Aug 21, 2026
867e24b
fix(risk): dry-run blocks every dangerous call, including malformed ones
fengtality Aug 21, 2026
e400715
chore(routines): drop the usdm routines from this branch
fengtality Aug 21, 2026
aec9271
docs(solana_dex_lp_expert): the lp_rebalancer controller's fields are…
fengtality Aug 21, 2026
fcd0974
fix(reload): derive the hot-reload module list instead of hand-mainta…
fengtality Aug 21, 2026
1f56ce8
chore(frontend): drop the dead TradingView script tag
fengtality Aug 21, 2026
bd3d9e2
docs(lp_range_config): position_offset_pct is a rebalance setting, ke…
fengtality Aug 21, 2026
cbd9d10
Merge main: PR #214 (ralph improvements) landed
fengtality Aug 21, 2026
118214b
chore(agents): move the solana_dex_lp_expert docs to the sample-agent…
fengtality Aug 21, 2026
479f987
docs(mcp): point swaps at order_executor; manage_gateway_swaps is the…
fengtality Aug 21, 2026
2e23b05
feat(mcp): register manage_gateway_config and manage_gateway_container
fengtality Aug 21, 2026
af41e34
fix(deps): lock the client commit that actually has the method condor…
fengtality Aug 21, 2026
1278f85
docs(mcp): a raw token address is a valid trading_pair side, no regis…
fengtality Aug 21, 2026
e75c212
chore(deps): re-lock hummingbot-api-client for save_network_pool params
fengtality Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ celerybeat.pid

# Environments
.env
# Every .env variant, not just the bare name. A `.env.bak-telegram` written by a
# config switcher carried a live OpenRouter key into a commit, and GitHub push
# protection is what caught it β€” the bare pattern below did not.
.env.*
*.bak-telegram
.venv
env/
venv/
Expand Down Expand Up @@ -195,3 +200,9 @@ agents/**/learnings.md
agents/**/sessions/
agents/**/dry_runs/
agents/**/delegations/

# Working notes on Gateway defects, edited by more than one session at a time.
# Tracking it made every `git add -A` sweep whichever session was mid-edit into the other
# one's commit β€” which is how 1529d04 came to carry a message describing work it does not
# contain. Kept on disk, out of the index.
GATEWAY_ISSUES.md
37 changes: 37 additions & 0 deletions condor/agents/providers/executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ async def execute(
)

running = [e for e in perf.executors if e["status"] == "RUNNING"]
# A terminal executor that still reports an on-chain position address has
# stranded live exposure with no automated owner: an involuntary hold
# (POSITION_HOLD with hold_reason set β€” an LP close that exhausted its
# retries) or a legacy FAILED-with-position. It would otherwise vanish
# from this RUNNING-only summary β€” surface it until it is recovered.
orphaned = [
e
for e in perf.executors
if e["status"] != "RUNNING"
and not (e.get("custom_info") or {}).get("orphan_resolved")
and (
(e.get("custom_info") or {}).get("orphaned_position")
or (
(e.get("custom_info") or {}).get("hold_reason")
and (e.get("custom_info") or {}).get("position_address")
)
or (
str(e.get("close_type") or "").upper() == "FAILED"
and (e.get("custom_info") or {}).get("position_address")
)
)
]
lines = [
(
f"Active Executors ({len(running)}) [agent: {agent_id}]:"
Expand All @@ -65,6 +87,20 @@ async def execute(
lines.append(
f" {r['pair']} {side} ${r['pnl']:+.2f} (V:${r['volume']:,.0f})"
)
for o in orphaned:
pos = (o.get("custom_info") or {}).get("position_address")
reason = (o.get("custom_info") or {}).get("hold_reason") or "close failed"
lines.append(
f" 🚨 ORPHANED POSITION: executor {o['id']} ({o['pair']}) terminated "
f"({reason}) with position {pos} still open on-chain. Stopping the executor "
"will NOT close it β€” it has already terminated. Close it with "
'manage_clmm(action="close", position_address=..., pool_address=...); '
'manage_executors(action="orphaned") reports the dex, pool and network for '
"the call, and pool_address is required. A new lp_executor CANNOT adopt the "
"position and would mint a second one. Then mark it recovered with "
f"manage_executors(action=\"resolve_orphan\", executor_id=\"{o['id']}\"). "
"Do not open new positions on these funds first."
)
if perf.bot_names:
lines.append(f" Bots operated: {', '.join(perf.bot_names)}")
lines.append(
Expand All @@ -90,6 +126,7 @@ async def execute(
name=self.name,
data={
"executors": running,
"orphaned_executors": orphaned,
"all_executors": perf.executors,
"total_pnl": perf.total_pnl,
"realized_pnl": perf.realized_pnl,
Expand Down
36 changes: 18 additions & 18 deletions condor/agents/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,25 +263,25 @@ async def callback(tool_call: dict, options: list[dict]) -> dict:
log.warning("Blocked %s: tool arguments could not be read", tool_name)
return {"outcome": {"outcome": "cancelled"}}

# Dry-run mode: block ALL mutating actions
# Dry-run mode: block ALL mutating actions.
#
# Everything here is already inside `is_dangerous_tool_call`, which
# did the per-action matching against DANGEROUS_*_ACTIONS. Repeating
# it per tool was not just redundant, it reopened SEC-093: that gate
# fails CLOSED on an unreadable action, so `manage_clmm` with a
# missing, null or non-string `action` arrives here as dangerous β€”
# and the re-check then read it as "" , matched no set, fell through
# every branch and hit the auto-approve tail. A malformed write
# executed for real in the one mode whose whole promise is that
# nothing does. Reaching this line is the decision; blocking is
# unconditional.
if execution_mode == "dry_run":
if tool_name == "manage_executors":
action = input_data.get("action", "")
if action in ("create", "stop"):
log.info("Dry-run mode: blocked manage_executors(%s)", action)
return {"outcome": {"outcome": "cancelled"}}
elif tool_name == "manage_bots":
action = input_data.get("action", "")
if action in DANGEROUS_BOT_ACTIONS:
log.info("Dry-run mode: blocked manage_bots(%s)", action)
return {"outcome": {"outcome": "cancelled"}}
elif tool_name in (
"place_order",
"manage_gateway_swaps",
"manage_gateway_clmm",
):
log.info("Dry-run mode: blocked %s", tool_name)
return {"outcome": {"outcome": "cancelled"}}
log.info(
"Dry-run mode: blocked %s(%s)",
tool_name,
input_data.get("action", "") or "?",
)
return {"outcome": {"outcome": "cancelled"}}

# For executor actions, run risk check
if tool_name == "manage_executors":
Expand Down
31 changes: 25 additions & 6 deletions condor/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ def _load_from_cm(user_data: Dict) -> None:
DEFAULT_DEX_NETWORK = "solana-mainnet-beta"
DEFAULT_DEX_CONNECTOR = "jupiter"
DEFAULT_DEX_PAIR = "SOL-USDC"
DEFAULT_DEX_SLIPPAGE = "1.0"
# No slippage default on purpose: an unset slippage is OMITTED from the Gateway request so the
# connector's own configured slippage applies. An explicit value (including "0") is a real value.
DEFAULT_DEX_SIDE = "BUY"
DEFAULT_DEX_AMOUNT = "1.0"

Expand Down Expand Up @@ -342,7 +343,6 @@ def _get_default_preferences() -> UserPreferences:
"dex": {
"default_network": DEFAULT_DEX_NETWORK,
"default_connector": DEFAULT_DEX_CONNECTOR,
"default_slippage": DEFAULT_DEX_SLIPPAGE,
"last_swap": {},
"last_pool": {},
},
Expand Down Expand Up @@ -635,6 +635,19 @@ def get_dex_connector(user_data: Dict, network: Optional[str] = None) -> str:
return get_dex_prefs(user_data).get("default_connector", DEFAULT_DEX_CONNECTOR)


def get_dex_slippage(user_data: Dict) -> Optional[str]:
"""Get the user's DEX slippage override, or None to use the connector's own setting"""
return get_dex_prefs(user_data).get("default_slippage")


def set_dex_slippage(user_data: Dict, slippage: str) -> None:
"""Set default DEX slippage percentage"""
prefs = _ensure_preferences(user_data)
prefs["dex"]["default_slippage"] = slippage
_sync_section_to_cm(user_data, "dex")
logger.info(f"Set DEX slippage to {slippage}%")


def get_dex_last_swap(user_data: Dict) -> DEXSwapParams:
"""Get last DEX swap parameters"""
return deepcopy(get_dex_prefs(user_data).get("last_swap", {}))
Expand Down Expand Up @@ -667,6 +680,9 @@ def get_dex_swap_defaults(user_data: Dict) -> DEXSwapParams:
1. System defaults
2. User's configured defaults
3. Last swap params (highest priority)

"slippage" is present ONLY when the user set one. Absent means "omit slippage_pct from the
Gateway request" so the connector's configured slippage applies.
"""
prefs = get_dex_prefs(user_data)
last_swap = prefs.get("last_swap", {})
Expand All @@ -675,17 +691,20 @@ def get_dex_swap_defaults(user_data: Dict) -> DEXSwapParams:
"network", prefs.get("default_network", DEFAULT_DEX_NETWORK)
)

return {
defaults: DEXSwapParams = {
"connector": last_swap.get("connector", get_dex_connector(user_data, network)),
"network": network,
"trading_pair": last_swap.get("trading_pair", DEFAULT_DEX_PAIR),
"side": DEFAULT_DEX_SIDE,
"amount": DEFAULT_DEX_AMOUNT,
"slippage": last_swap.get(
"slippage", prefs.get("default_slippage", DEFAULT_DEX_SLIPPAGE)
),
}

slippage = last_swap.get("slippage", prefs.get("default_slippage"))
if slippage is not None:
defaults["slippage"] = slippage

return defaults


# ============================================
# PUBLIC API - GENERAL
Expand Down
103 changes: 92 additions & 11 deletions condor/runtime/danger.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
DANGEROUS_TOOLS = {
"place_order",
"manage_gateway_swaps", # execute action
"manage_gateway_clmm", # open/close position
"manage_clmm", # every action that moves liquidity
"manage_amm", # every action that moves liquidity
"manage_gateway_config", # only the wallets resource; see below
}

# Tools that are always blocked (RBAC bypass prevention)
Expand All @@ -45,8 +47,29 @@
# Actions within manage_gateway_swaps that require confirmation
DANGEROUS_SWAP_ACTIONS = {"execute"}

# Actions within manage_gateway_clmm that require confirmation
DANGEROUS_CLMM_ACTIONS = {"open_position", "close_position"}
# Actions within manage_clmm that require confirmation. These are the tool's
# own action literals β€” a name that does not match one lets the call through
# ungated, so they are asserted against the registered tool in the tests.
DANGEROUS_CLMM_ACTIONS = {
"open",
"close",
"add_liquidity",
"remove_liquidity",
"collect_fees",
"create_pool",
}

# Actions within manage_amm that require confirmation
DANGEROUS_AMM_ACTIONS = {"add_liquidity", "remove_liquidity", "create_pool"}

# Resource types within manage_gateway_config that require confirmation. This tool
# is gated on `resource_type`, not `action`, because what it edits matters and how
# it edits does not: `wallets` + `add` takes a PRIVATE KEY, and `delete` removes a
# signing wallet. Everything else it touches β€” tokens, pools, connectors, networks β€”
# is Gateway's own symbol/address mapping. Deleting a token there moves no funds and
# changes nothing on-chain, so gating it would put a human in front of a config edit
# while the trades that edit enables stay where they are.
DANGEROUS_CONFIG_RESOURCES = {"wallets"}


def tool_call_name(tool_call: dict[str, Any]) -> str:
Expand Down Expand Up @@ -104,6 +127,24 @@ def _has_dangerous_action(
return action in dangerous_actions


def _has_dangerous_resource(
tool_call: dict[str, Any], dangerous_resources: set[str]
) -> bool:
"""Whether a resource-gated tool call selects one of its dangerous resources.

The resource-typed twin of :func:`_has_dangerous_action`, and it fails closed the
same way (SEC-093): unreadable arguments, or a missing/non-string ``resource_type``,
count as dangerous.
"""
input_data = tool_call_input(tool_call)
if input_data is None:
return True
resource = input_data.get("resource_type")
if not isinstance(resource, str) or not resource:
return True
return resource in dangerous_resources


def is_dangerous_tool_call(tool_call: dict[str, Any]) -> bool:
"""Check if a tool call requires user confirmation."""
tool_name = tool_call_name(tool_call)
Expand All @@ -114,10 +155,16 @@ def is_dangerous_tool_call(tool_call: dict[str, Any]) -> bool:
if tool_name == "manage_gateway_swaps":
return _has_dangerous_action(tool_call, DANGEROUS_SWAP_ACTIONS)

# For manage_gateway_clmm, only open/close are dangerous
if tool_name == "manage_gateway_clmm":
# For the LP tools, only the actions that move liquidity are dangerous
if tool_name == "manage_clmm":
return _has_dangerous_action(tool_call, DANGEROUS_CLMM_ACTIONS)

if tool_name == "manage_amm":
return _has_dangerous_action(tool_call, DANGEROUS_AMM_ACTIONS)

if tool_name == "manage_gateway_config":
return _has_dangerous_resource(tool_call, DANGEROUS_CONFIG_RESOURCES)

return True

# manage_executors with create/stop actions
Expand Down Expand Up @@ -186,13 +233,47 @@ def format_tool_summary(tool_call: dict[str, Any]) -> str:
amount = input_data.get("amount", "?")
return f"Swap {side} {amount} {pair}"

if tool_name == "manage_gateway_clmm":
if tool_name == "manage_gateway_config":
resource = input_data.get("resource_type", "?")
action = input_data.get("action", "?")
if resource == "wallets":
if action == "add":
chain = input_data.get("chain", "?")
return f"Import a {chain} wallet into Gateway (private key)"
if action == "delete":
addr = str(input_data.get("wallet_address") or "?")
return f"Remove wallet {addr[:12]}... from Gateway"
return f"Gateway config: {action} {resource}"

if tool_name in ("manage_clmm", "manage_amm"):
action = input_data.get("action", "?")
if action == "open_position":
return "Open LP position"
if action == "close_position":
return "Close LP position"
return f"CLMM: {action}"
kind = "CLMM" if tool_name == "manage_clmm" else "AMM"
connector = input_data.get("connector", "?")
pool = (
input_data.get("pool_address") or input_data.get("position_address") or "?"
)
if action == "open":
lower = input_data.get("lower_price", "?")
upper = input_data.get("upper_price", "?")
return (
f"Open {kind} position on {connector} pool {pool} over {lower}-{upper}"
)
if action == "close":
return f"Close {kind} position {pool} on {connector}"
if action == "add_liquidity":
base = input_data.get("base_token_amount", "?")
quote = input_data.get("quote_token_amount", "?")
return f"Add {base} base / {quote} quote to {kind} {pool} on {connector}"
if action == "remove_liquidity":
pct = input_data.get("percentage_to_remove", "?")
return f"Remove {pct}% from {kind} position {pool} on {connector}"
if action == "collect_fees":
return f"Collect fees from {kind} position {pool} on {connector}"
if action == "create_pool":
base = input_data.get("base_token", "?")
quote = input_data.get("quote_token", "?")
return f"Create {kind} pool {base}-{quote} on {connector}"
return f"{kind}: {action}"

# Generic fallback
return tool_name
2 changes: 0 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
</head>
<body>
<div id="root"></div>
<!-- TradingView Charting Library (place charting_library/ folder in public/) -->
<script src="/charting_library/charting_library.standalone.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions handlers/agents/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
)
from condor.runtime.danger import ( # noqa: F401
BLOCKED_TOOLS,
DANGEROUS_AMM_ACTIONS,
DANGEROUS_BOT_ACTIONS,
DANGEROUS_CLMM_ACTIONS,
DANGEROUS_CONFIG_RESOURCES,
DANGEROUS_EXECUTOR_ACTIONS,
DANGEROUS_SWAP_ACTIONS,
DANGEROUS_TOOLS,
Expand Down
Loading
Loading