arc-devkit 0.4.6__tar.gz → 0.4.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/PKG-INFO +11 -8
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/README.md +0 -2
- arc_devkit-0.4.7/arc_devkit/agents/autonomous.py +108 -0
- arc_devkit-0.4.7/arc_devkit/agents/coordinator.py +128 -0
- arc_devkit-0.4.7/arc_devkit/agents/dashboard.py +64 -0
- arc_devkit-0.4.7/arc_devkit/agents/event_bus.py +66 -0
- arc_devkit-0.4.7/arc_devkit/agents/guardrails.py +162 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/agents/monitor_agent.py +95 -1
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/agents/payment_agent.py +158 -1
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/analytics/portfolio.py +8 -1
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/api/main.py +56 -8
- arc_devkit-0.4.7/arc_devkit/api/rate_limit.py +7 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/api/routes/agents.py +24 -7
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/api/routes/copilot.py +43 -4
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/api/routes/debugger.py +13 -2
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/commands/agent.py +100 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/commands/copilot.py +44 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/flat.py +73 -4
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/config.py +47 -1
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/contracts/loader.py +4 -2
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/copilot/agent.py +113 -8
- arc_devkit-0.4.7/arc_devkit/copilot/tools.py +187 -0
- arc_devkit-0.4.7/arc_devkit/core/validation.py +114 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/debugger/tx_analyzer.py +22 -10
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit.egg-info/PKG-INFO +11 -8
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit.egg-info/SOURCES.txt +15 -1
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit.egg-info/requires.txt +5 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/pyproject.toml +13 -6
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_analytics.py +1 -1
- arc_devkit-0.4.7/tests/test_api_security.py +134 -0
- arc_devkit-0.4.7/tests/test_autonomous.py +264 -0
- arc_devkit-0.4.7/tests/test_copilot_tools.py +161 -0
- arc_devkit-0.4.7/tests/test_guardrails.py +90 -0
- arc_devkit-0.4.7/tests/test_payment_security.py +164 -0
- arc_devkit-0.4.7/tests/test_validation.py +96 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/agents/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/agents/async_base.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/agents/async_monitor.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/agents/base_agent.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/analytics/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/api/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/api/routes/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/commands/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/commands/debug.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/cli/main.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/contracts/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/copilot/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/core/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/core/connection.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/core/gas.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/core/wallet.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/debugger/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/deploy/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/deploy/deployer.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/events/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/events/listener.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/usdc/__init__.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit/usdc/token.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit.egg-info/dependency_links.txt +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit.egg-info/entry_points.txt +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/arc_devkit.egg-info/top_level.txt +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/setup.cfg +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_agents.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_api.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_async_agents.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_cli.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_connection.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_contracts.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_copilot.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_debugger.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_deploy.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_events.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_integration.py +0 -0
- {arc_devkit-0.4.6 → arc_devkit-0.4.7}/tests/test_usdc.py +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arc-devkit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
4
4
|
Summary: Plataforma open source de ferramentas para desenvolvedores da Arc blockchain (Circle)
|
|
5
|
-
Author
|
|
6
|
-
License
|
|
7
|
-
Project-URL: Homepage, https://github.com/Jeielsantosdev/
|
|
8
|
-
Project-URL: Repository, https://github.com/Jeielsantosdev/
|
|
9
|
-
Project-URL:
|
|
5
|
+
Author: Arc DevKit Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jeielsantosdev/arc_dev_kit
|
|
8
|
+
Project-URL: Repository, https://github.com/Jeielsantosdev/arc_dev_kit
|
|
9
|
+
Project-URL: Issues, https://github.com/Jeielsantosdev/arc_dev_kit/issues
|
|
10
10
|
Keywords: arc,blockchain,circle,usdc,web3,ai,anthropic
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
14
|
Classifier: Programming Language :: Python :: 3
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -35,7 +36,11 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
|
35
36
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
36
37
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
37
38
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
39
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
40
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
38
41
|
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
42
|
+
Provides-Extra: security
|
|
43
|
+
Requires-Dist: keyring>=24; extra == "security"
|
|
39
44
|
|
|
40
45
|
# Arc DevKit
|
|
41
46
|
|
|
@@ -48,8 +53,6 @@ Requires-Dist: httpx>=0.27; extra == "dev"
|
|
|
48
53
|
|
|
49
54
|
**Arc DevKit** is a complete Python SDK for developers building on the **Arc blockchain** — Circle's EVM-compatible Layer 1 with USDC as the gas token and sub-second finality.
|
|
50
55
|
|
|
51
|
-
**link to project** https://arc-dev-kit-uxun.vercel.app/
|
|
52
|
-
|
|
53
56
|
It solves the practical friction of building on Arc: USDC gas accounting, PoA middleware, ERC-20 monitoring, async agents, WebSocket streaming, and AI-assisted debugging — all packaged and ready to use.
|
|
54
57
|
|
|
55
58
|
---
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
**Arc DevKit** is a complete Python SDK for developers building on the **Arc blockchain** — Circle's EVM-compatible Layer 1 with USDC as the gas token and sub-second finality.
|
|
11
11
|
|
|
12
|
-
**link to project** https://arc-dev-kit-uxun.vercel.app/
|
|
13
|
-
|
|
14
12
|
It solves the practical friction of building on Arc: USDC gas accounting, PoA middleware, ERC-20 monitoring, async agents, WebSocket streaming, and AI-assisted debugging — all packaged and ready to use.
|
|
15
13
|
|
|
16
14
|
---
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Autonomous agent behaviors — actions that run without a human in the loop.
|
|
2
|
+
|
|
3
|
+
Every autonomous behavior REQUIRES Guardrails: kill switch, recipient whitelist,
|
|
4
|
+
and daily spend limit are enforced on each action, and every execution is
|
|
5
|
+
recorded in the audit log.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
|
|
11
|
+
from arc_devkit.agents.guardrails import Guardrails
|
|
12
|
+
from arc_devkit.agents.monitor_agent import MonitorAgent
|
|
13
|
+
from arc_devkit.agents.payment_agent import PaymentAgent
|
|
14
|
+
from arc_devkit.core.validation import validate_address, validate_amount
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AutoRefueler:
|
|
20
|
+
"""
|
|
21
|
+
Keeps a target address funded: when its native balance drops below a
|
|
22
|
+
threshold, a PaymentAgent automatically tops it up.
|
|
23
|
+
|
|
24
|
+
The refuel transfer goes through all guardrails (kill switch, whitelist,
|
|
25
|
+
daily spend limit) and mandatory simulation before broadcast.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
guardrails = Guardrails.from_settings()
|
|
29
|
+
refueler = AutoRefueler(
|
|
30
|
+
target_address="0xServiceWallet...",
|
|
31
|
+
threshold_usdc=Decimal("0.5"),
|
|
32
|
+
top_up_amount_usdc=Decimal("1.0"),
|
|
33
|
+
payment_agent=PaymentAgent(guardrails=guardrails),
|
|
34
|
+
guardrails=guardrails,
|
|
35
|
+
)
|
|
36
|
+
monitor = MonitorAgent(watched_address="0xServiceWallet...", interval_seconds=30)
|
|
37
|
+
refueler.attach(monitor)
|
|
38
|
+
monitor.execute()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
target_address: str,
|
|
44
|
+
threshold_usdc: Decimal,
|
|
45
|
+
top_up_amount_usdc: Decimal,
|
|
46
|
+
payment_agent: PaymentAgent,
|
|
47
|
+
guardrails: Guardrails,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Args:
|
|
51
|
+
target_address: Address to keep funded.
|
|
52
|
+
threshold_usdc: Refuel when balance drops below this value.
|
|
53
|
+
top_up_amount_usdc: Amount transferred on each refuel.
|
|
54
|
+
payment_agent: Funded PaymentAgent used to send the top-up.
|
|
55
|
+
guardrails: Required autonomy guardrails.
|
|
56
|
+
"""
|
|
57
|
+
self._target = validate_address(target_address)
|
|
58
|
+
self._threshold = validate_amount(threshold_usdc)
|
|
59
|
+
self._top_up = validate_amount(top_up_amount_usdc)
|
|
60
|
+
self._payment_agent = payment_agent
|
|
61
|
+
self._guardrails = guardrails
|
|
62
|
+
# Ensure the payment agent enforces the same guardrails
|
|
63
|
+
if self._payment_agent._guardrails is None:
|
|
64
|
+
self._payment_agent._guardrails = guardrails
|
|
65
|
+
|
|
66
|
+
def attach(self, monitor: MonitorAgent) -> None:
|
|
67
|
+
"""Register the refuel action on a monitor's low-balance trigger."""
|
|
68
|
+
threshold_wei = int(self._threshold * Decimal(10**18))
|
|
69
|
+
monitor.on_low_balance(threshold_wei, self._refuel)
|
|
70
|
+
logger.info(
|
|
71
|
+
"AutoRefueler attached: %s below %s USDC → top up %s USDC",
|
|
72
|
+
self._target[:10],
|
|
73
|
+
self._threshold,
|
|
74
|
+
self._top_up,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _refuel(self, event: dict) -> None:
|
|
78
|
+
"""Trigger action: execute the guarded top-up transfer."""
|
|
79
|
+
address = event.get("address", self._target)
|
|
80
|
+
logger.info(
|
|
81
|
+
"AutoRefueler firing for %s (balance %s wei)", address, event.get("balance_wei")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
result = self._payment_agent.execute(
|
|
85
|
+
to=self._target,
|
|
86
|
+
amount_usdc=float(self._top_up),
|
|
87
|
+
enviar=True,
|
|
88
|
+
wait_receipt=False,
|
|
89
|
+
trigger="on_low_balance",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
status = result.get("status")
|
|
93
|
+
if status in {"blocked", "error", "simulation_failed"}:
|
|
94
|
+
logger.warning("AutoRefueler blocked/failed: %s", result.get("error"))
|
|
95
|
+
self._guardrails.audit(
|
|
96
|
+
agent="AutoRefueler",
|
|
97
|
+
trigger="on_low_balance",
|
|
98
|
+
action="refuel_denied",
|
|
99
|
+
target=self._target,
|
|
100
|
+
reason=result.get("error", status),
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
logger.info(
|
|
104
|
+
"AutoRefueler sent %s USDC → %s (%s)",
|
|
105
|
+
self._top_up,
|
|
106
|
+
self._target,
|
|
107
|
+
result.get("tx_hash"),
|
|
108
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Coordinator agent — plans multi-agent workflows from natural-language goals.
|
|
2
|
+
|
|
3
|
+
The coordinator uses the DevCopilot in agentic mode (read-only tools) to inspect
|
|
4
|
+
on-chain state and produce an execution plan. Steps that move funds are only
|
|
5
|
+
executed when a PaymentAgent with Guardrails is registered — and every action
|
|
6
|
+
passes the kill switch, whitelist, and spend-limit checks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from arc_devkit.agents.event_bus import EventBus
|
|
12
|
+
from arc_devkit.agents.guardrails import Guardrails
|
|
13
|
+
from arc_devkit.agents.monitor_agent import MonitorAgent
|
|
14
|
+
from arc_devkit.agents.payment_agent import PaymentAgent
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_PLAN_PROMPT = """\
|
|
19
|
+
You are coordinating economic agents on the Arc blockchain. Analyze the goal
|
|
20
|
+
below using your read-only tools (balances, gas, block info) and produce a plan.
|
|
21
|
+
|
|
22
|
+
Goal: {goal}
|
|
23
|
+
|
|
24
|
+
Respond with:
|
|
25
|
+
1. **Current state** — relevant on-chain data you gathered
|
|
26
|
+
2. **Plan** — numbered steps; mark each step as [read-only] or [requires-funds]
|
|
27
|
+
3. **Risks** — what could go wrong and which guardrails apply
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CoordinatorAgent:
|
|
32
|
+
"""
|
|
33
|
+
Orchestrates DevCopilot, PaymentAgent, and MonitorAgent around a goal.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
coordinator = CoordinatorAgent(guardrails=Guardrails.from_settings())
|
|
37
|
+
coordinator.register_payment_agent(PaymentAgent(guardrails=guardrails))
|
|
38
|
+
result = coordinator.plan("keep 0xAbc... above 0.5 USDC")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, guardrails: Guardrails | None = None, bus: EventBus | None = None) -> None:
|
|
42
|
+
self._guardrails = guardrails or Guardrails.from_settings()
|
|
43
|
+
self.bus = bus or EventBus()
|
|
44
|
+
self._payment_agent: PaymentAgent | None = None
|
|
45
|
+
self._monitors: list[MonitorAgent] = []
|
|
46
|
+
|
|
47
|
+
def register_payment_agent(self, agent: PaymentAgent) -> None:
|
|
48
|
+
"""Register the payment agent used for fund-moving steps (guardrails enforced)."""
|
|
49
|
+
if agent._guardrails is None:
|
|
50
|
+
agent._guardrails = self._guardrails
|
|
51
|
+
self._payment_agent = agent
|
|
52
|
+
|
|
53
|
+
def register_monitor(self, monitor: MonitorAgent) -> None:
|
|
54
|
+
"""Register a monitor whose events feed the coordinator's bus."""
|
|
55
|
+
self._monitors.append(monitor)
|
|
56
|
+
|
|
57
|
+
def plan(self, goal: str, max_iterations: int = 10) -> dict:
|
|
58
|
+
"""
|
|
59
|
+
Produce an execution plan for a natural-language goal.
|
|
60
|
+
|
|
61
|
+
Uses DevCopilot agentic mode with read-only tools; no funds are moved.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict with 'response' (the plan), 'tool_calls', and 'iterations'.
|
|
65
|
+
"""
|
|
66
|
+
from arc_devkit.copilot.agent import DevCopilot
|
|
67
|
+
|
|
68
|
+
self._guardrails.check_kill_switch()
|
|
69
|
+
copilot = DevCopilot()
|
|
70
|
+
result = copilot.run_agent(_PLAN_PROMPT.format(goal=goal), max_iterations=max_iterations)
|
|
71
|
+
self.bus.publish_sync("coordinator.plan", {"goal": goal, "plan": result["response"]})
|
|
72
|
+
self._guardrails.audit(
|
|
73
|
+
agent="CoordinatorAgent",
|
|
74
|
+
trigger="plan",
|
|
75
|
+
action="generate_plan",
|
|
76
|
+
goal=goal,
|
|
77
|
+
tool_calls=len(result["tool_calls"]),
|
|
78
|
+
)
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
def execute_transfer(self, to: str, amount_usdc: float, trigger: str = "coordinator") -> dict:
|
|
82
|
+
"""
|
|
83
|
+
Execute a fund-moving step through the registered (guarded) PaymentAgent.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The PaymentAgent result dict; status "blocked" when a guardrail denies it.
|
|
87
|
+
"""
|
|
88
|
+
if self._payment_agent is None:
|
|
89
|
+
return {"status": "error", "error": "No PaymentAgent registered."}
|
|
90
|
+
result = self._payment_agent.execute(
|
|
91
|
+
to=to, amount_usdc=amount_usdc, enviar=True, trigger=trigger
|
|
92
|
+
)
|
|
93
|
+
self.bus.publish_sync("coordinator.transfer", {"to": to, "result": result})
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
def maintain_balance(
|
|
97
|
+
self,
|
|
98
|
+
address: str,
|
|
99
|
+
min_usdc: float,
|
|
100
|
+
top_up_usdc: float,
|
|
101
|
+
interval_seconds: int = 30,
|
|
102
|
+
max_iterations: int = 0,
|
|
103
|
+
) -> dict:
|
|
104
|
+
"""
|
|
105
|
+
Declarative workflow: keep an address above a minimum balance.
|
|
106
|
+
|
|
107
|
+
Wires an AutoRefueler to a MonitorAgent and runs the monitoring loop
|
|
108
|
+
(blocking). Requires a registered PaymentAgent with funds.
|
|
109
|
+
"""
|
|
110
|
+
from decimal import Decimal
|
|
111
|
+
|
|
112
|
+
from arc_devkit.agents.autonomous import AutoRefueler
|
|
113
|
+
|
|
114
|
+
if self._payment_agent is None:
|
|
115
|
+
return {"status": "error", "error": "No PaymentAgent registered."}
|
|
116
|
+
|
|
117
|
+
monitor = MonitorAgent(watched_address=address, interval_seconds=interval_seconds)
|
|
118
|
+
refueler = AutoRefueler(
|
|
119
|
+
target_address=address,
|
|
120
|
+
threshold_usdc=Decimal(str(min_usdc)),
|
|
121
|
+
top_up_amount_usdc=Decimal(str(top_up_usdc)),
|
|
122
|
+
payment_agent=self._payment_agent,
|
|
123
|
+
guardrails=self._guardrails,
|
|
124
|
+
)
|
|
125
|
+
refueler.attach(monitor)
|
|
126
|
+
self.register_monitor(monitor)
|
|
127
|
+
logger.info("maintain_balance workflow started for %s", address)
|
|
128
|
+
return monitor.execute(max_iterations=max_iterations)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Live terminal dashboard for agent state — built on rich.Live."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from rich.live import Live
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from arc_devkit.agents.monitor_agent import MonitorAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentDashboard:
|
|
13
|
+
"""
|
|
14
|
+
Renders watched-wallet balances and recent events in a live terminal panel.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
monitor = MonitorAgent(watched_addresses=[...], interval_seconds=10)
|
|
18
|
+
AgentDashboard(monitor).run(max_iterations=0)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
MAX_EVENTS = 8
|
|
22
|
+
|
|
23
|
+
def __init__(self, monitor: MonitorAgent) -> None:
|
|
24
|
+
self._monitor = monitor
|
|
25
|
+
self._events: list[dict] = []
|
|
26
|
+
|
|
27
|
+
def _on_event(self, event: dict) -> None:
|
|
28
|
+
self._events.append({**event, "_at": datetime.now().strftime("%H:%M:%S")})
|
|
29
|
+
self._events = self._events[-self.MAX_EVENTS :]
|
|
30
|
+
|
|
31
|
+
def _render(self) -> Table:
|
|
32
|
+
table = Table(title="Arc Agents — Live", header_style="bold cyan")
|
|
33
|
+
table.add_column("Wallet", style="cyan")
|
|
34
|
+
table.add_column("Balance (USDC)", justify="right")
|
|
35
|
+
|
|
36
|
+
balances = self._monitor.get_balance()
|
|
37
|
+
for addr, info in balances.items():
|
|
38
|
+
wei = Decimal(info["balance_wei"])
|
|
39
|
+
table.add_row(f"{addr[:10]}…{addr[-6:]}", str(wei / Decimal(10**18)))
|
|
40
|
+
|
|
41
|
+
if self._events:
|
|
42
|
+
table.add_section()
|
|
43
|
+
table.add_row("[bold]Recent events[/bold]", "")
|
|
44
|
+
for ev in reversed(self._events):
|
|
45
|
+
kind = ev.get("event_type", "native")
|
|
46
|
+
sign = "+" if ev.get("type") == "credit" else "-"
|
|
47
|
+
amount = ev.get("change_wei") or ev.get("value_atomic") or "?"
|
|
48
|
+
table.add_row(
|
|
49
|
+
f"[dim]{ev['_at']}[/dim] {kind}",
|
|
50
|
+
f"{sign}{amount}",
|
|
51
|
+
)
|
|
52
|
+
return table
|
|
53
|
+
|
|
54
|
+
def run(self, max_iterations: int = 0, refresh_per_second: int = 2) -> dict:
|
|
55
|
+
"""Run the monitor loop while rendering the live panel (blocking)."""
|
|
56
|
+
with Live(self._render(), refresh_per_second=refresh_per_second) as live:
|
|
57
|
+
|
|
58
|
+
def _callback(event: dict) -> None:
|
|
59
|
+
self._on_event(event)
|
|
60
|
+
live.update(self._render())
|
|
61
|
+
|
|
62
|
+
result = self._monitor.execute(callback=_callback, max_iterations=max_iterations)
|
|
63
|
+
live.update(self._render())
|
|
64
|
+
return result
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Async event bus for inter-agent communication.
|
|
2
|
+
|
|
3
|
+
Lightweight publish/subscribe over asyncio: agents publish events to topics
|
|
4
|
+
("balance.low", "payment.sent", ...) and other agents subscribe with async or
|
|
5
|
+
sync handlers. Handler failures are isolated and logged.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventBus:
|
|
18
|
+
"""In-process pub/sub bus for agent coordination."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._subscribers: dict[str, list[Callable[[dict], Any]]] = {}
|
|
22
|
+
self._history: list[tuple[str, dict]] = []
|
|
23
|
+
|
|
24
|
+
def subscribe(self, topic: str, handler: Callable[[dict], Any]) -> None:
|
|
25
|
+
"""Register a sync or async handler for a topic."""
|
|
26
|
+
self._subscribers.setdefault(topic, []).append(handler)
|
|
27
|
+
logger.debug("Subscribed %s to topic %r", getattr(handler, "__name__", handler), topic)
|
|
28
|
+
|
|
29
|
+
def unsubscribe(self, topic: str, handler: Callable[[dict], Any]) -> None:
|
|
30
|
+
"""Remove a handler from a topic (no-op if absent)."""
|
|
31
|
+
handlers = self._subscribers.get(topic, [])
|
|
32
|
+
if handler in handlers:
|
|
33
|
+
handlers.remove(handler)
|
|
34
|
+
|
|
35
|
+
async def publish(self, topic: str, event: dict) -> int:
|
|
36
|
+
"""
|
|
37
|
+
Deliver an event to all subscribers of a topic.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Number of handlers that received the event.
|
|
41
|
+
"""
|
|
42
|
+
self._history.append((topic, event))
|
|
43
|
+
handlers = list(self._subscribers.get(topic, []))
|
|
44
|
+
for handler in handlers:
|
|
45
|
+
try:
|
|
46
|
+
result = handler(event)
|
|
47
|
+
if inspect.isawaitable(result):
|
|
48
|
+
await result
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
logger.error("Event handler failed on topic %r: %s", topic, exc)
|
|
51
|
+
return len(handlers)
|
|
52
|
+
|
|
53
|
+
def publish_sync(self, topic: str, event: dict) -> int:
|
|
54
|
+
"""Publish from synchronous code (runs its own event loop tick)."""
|
|
55
|
+
try:
|
|
56
|
+
loop = asyncio.get_running_loop()
|
|
57
|
+
except RuntimeError:
|
|
58
|
+
return asyncio.run(self.publish(topic, event))
|
|
59
|
+
task = loop.create_task(self.publish(topic, event))
|
|
60
|
+
del task # fire-and-forget inside a running loop
|
|
61
|
+
return len(self._subscribers.get(topic, []))
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def history(self) -> list[tuple[str, dict]]:
|
|
65
|
+
"""Copy of all published (topic, event) pairs."""
|
|
66
|
+
return list(self._history)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Guardrails for autonomous agent actions — spend limits, whitelists, audit log, kill switch.
|
|
2
|
+
|
|
3
|
+
Any action executed without a human in the loop must pass through these checks:
|
|
4
|
+
|
|
5
|
+
guardrails = Guardrails.from_settings()
|
|
6
|
+
guardrails.check_recipient(to)
|
|
7
|
+
guardrails.check_spend(amount)
|
|
8
|
+
... broadcast ...
|
|
9
|
+
guardrails.record_spend(amount)
|
|
10
|
+
guardrails.audit(agent="PaymentAgent", trigger="on_low_balance", action="transfer", ...)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from decimal import Decimal
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from arc_devkit.core.validation import validate_address
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_STATE_DIR = Path.home() / ".arc_devkit"
|
|
24
|
+
_KILL_SWITCH_FILE = _STATE_DIR / "agents.stop"
|
|
25
|
+
_SPEND_FILE = _STATE_DIR / "spend_tracker.json"
|
|
26
|
+
_AUDIT_FILE = _STATE_DIR / "audit.log"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GuardrailViolation(Exception):
|
|
30
|
+
"""Raised when an autonomous action violates a configured guardrail."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def kill_switch_active(state_dir: Path | None = None) -> bool:
|
|
34
|
+
"""True when the kill switch file exists — all autonomous agents must halt."""
|
|
35
|
+
path = (state_dir / "agents.stop") if state_dir else _KILL_SWITCH_FILE
|
|
36
|
+
return path.exists()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def activate_kill_switch(state_dir: Path | None = None) -> Path:
|
|
40
|
+
"""Create the kill switch file, halting all autonomous agents."""
|
|
41
|
+
path = (state_dir / "agents.stop") if state_dir else _KILL_SWITCH_FILE
|
|
42
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
path.write_text(datetime.now(UTC).isoformat())
|
|
44
|
+
logger.warning("Kill switch ACTIVATED: %s", path)
|
|
45
|
+
return path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def clear_kill_switch(state_dir: Path | None = None) -> bool:
|
|
49
|
+
"""Remove the kill switch file; returns True if it existed."""
|
|
50
|
+
path = (state_dir / "agents.stop") if state_dir else _KILL_SWITCH_FILE
|
|
51
|
+
if path.exists():
|
|
52
|
+
path.unlink()
|
|
53
|
+
logger.info("Kill switch cleared: %s", path)
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Guardrails:
|
|
59
|
+
"""
|
|
60
|
+
Enforces autonomy limits for agents that act without human confirmation.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
max_spend_per_day_usdc: Daily spend ceiling; None disables the check.
|
|
64
|
+
allowed_recipients: Whitelist of recipient addresses; empty allows any
|
|
65
|
+
(a warning is logged — configure a whitelist in production).
|
|
66
|
+
state_dir: Directory for the spend tracker, audit log, and kill switch.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
max_spend_per_day_usdc: Decimal | None = None,
|
|
72
|
+
allowed_recipients: tuple[str, ...] | list[str] = (),
|
|
73
|
+
state_dir: str | Path | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._max_spend = max_spend_per_day_usdc
|
|
76
|
+
self._whitelist = {validate_address(a).lower() for a in allowed_recipients}
|
|
77
|
+
self._state_dir = Path(state_dir) if state_dir else _STATE_DIR
|
|
78
|
+
self._state_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_settings(cls) -> "Guardrails":
|
|
82
|
+
"""Build guardrails from MAX_SPEND_PER_DAY_USDC and AGENT_ALLOWED_RECIPIENTS."""
|
|
83
|
+
from arc_devkit.config import settings
|
|
84
|
+
|
|
85
|
+
return cls(
|
|
86
|
+
max_spend_per_day_usdc=settings.max_spend_per_day_usdc,
|
|
87
|
+
allowed_recipients=settings.agent_allowed_recipients,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# Checks
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def check_kill_switch(self) -> None:
|
|
95
|
+
"""Raise if the kill switch is active."""
|
|
96
|
+
if kill_switch_active(self._state_dir):
|
|
97
|
+
raise GuardrailViolation(
|
|
98
|
+
"Kill switch is active — autonomous actions are halted. "
|
|
99
|
+
"Run 'arcdevkit agent resume' to re-enable."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def check_recipient(self, to: str) -> None:
|
|
103
|
+
"""Raise if the recipient is not on the configured whitelist."""
|
|
104
|
+
if not self._whitelist:
|
|
105
|
+
logger.warning(
|
|
106
|
+
"No recipient whitelist configured (AGENT_ALLOWED_RECIPIENTS) — "
|
|
107
|
+
"autonomous transfers are unrestricted by destination."
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
if validate_address(to).lower() not in self._whitelist:
|
|
111
|
+
raise GuardrailViolation(f"Recipient {to} is not on the autonomous-agent whitelist.")
|
|
112
|
+
|
|
113
|
+
def _spent_today(self) -> Decimal:
|
|
114
|
+
spend_file = self._state_dir / "spend_tracker.json"
|
|
115
|
+
today = datetime.now(UTC).date().isoformat()
|
|
116
|
+
if spend_file.exists():
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(spend_file.read_text())
|
|
119
|
+
if data.get("date") == today:
|
|
120
|
+
return Decimal(data.get("spent", "0"))
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.warning("Failed to read spend tracker: %s", exc)
|
|
123
|
+
return Decimal("0")
|
|
124
|
+
|
|
125
|
+
def check_spend(self, amount: Decimal) -> None:
|
|
126
|
+
"""Raise if amount would exceed the daily spend ceiling."""
|
|
127
|
+
if self._max_spend is None:
|
|
128
|
+
return
|
|
129
|
+
spent = self._spent_today()
|
|
130
|
+
if spent + amount > self._max_spend:
|
|
131
|
+
raise GuardrailViolation(
|
|
132
|
+
f"Daily spend limit exceeded: {spent} + {amount} > "
|
|
133
|
+
f"{self._max_spend} USDC (MAX_SPEND_PER_DAY_USDC)."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def record_spend(self, amount: Decimal) -> None:
|
|
137
|
+
"""Persist a completed spend against today's counter."""
|
|
138
|
+
spend_file = self._state_dir / "spend_tracker.json"
|
|
139
|
+
today = datetime.now(UTC).date().isoformat()
|
|
140
|
+
total = self._spent_today() + amount
|
|
141
|
+
spend_file.write_text(json.dumps({"date": today, "spent": str(total)}))
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# Audit
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def audit(self, agent: str, trigger: str, action: str, **details: object) -> None:
|
|
148
|
+
"""Append an autonomous-action record to the audit log (JSON lines)."""
|
|
149
|
+
record = {
|
|
150
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
151
|
+
"agent": agent,
|
|
152
|
+
"trigger": trigger,
|
|
153
|
+
"action": action,
|
|
154
|
+
**{k: str(v) for k, v in details.items()},
|
|
155
|
+
}
|
|
156
|
+
audit_file = self._state_dir / "audit.log"
|
|
157
|
+
try:
|
|
158
|
+
with audit_file.open("a") as fh:
|
|
159
|
+
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
logger.error("Failed to write audit log: %s", exc)
|
|
162
|
+
logger.info("AUDIT %s", record)
|