arc-devkit 0.4.6__tar.gz → 0.8.0__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.
Files changed (132) hide show
  1. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/PKG-INFO +10 -1
  2. arc_devkit-0.8.0/arc_devkit/agents/autonomous.py +108 -0
  3. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/agents/base_agent.py +9 -0
  4. arc_devkit-0.8.0/arc_devkit/agents/coordinator.py +167 -0
  5. arc_devkit-0.8.0/arc_devkit/agents/dashboard.py +64 -0
  6. arc_devkit-0.8.0/arc_devkit/agents/event_bus.py +66 -0
  7. arc_devkit-0.8.0/arc_devkit/agents/guardrails.py +162 -0
  8. arc_devkit-0.8.0/arc_devkit/agents/identity.py +205 -0
  9. arc_devkit-0.8.0/arc_devkit/agents/job_agent.py +88 -0
  10. arc_devkit-0.8.0/arc_devkit/agents/jobs.py +321 -0
  11. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/agents/monitor_agent.py +137 -1
  12. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/agents/payment_agent.py +178 -2
  13. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/analytics/portfolio.py +108 -2
  14. arc_devkit-0.8.0/arc_devkit/api/auth.py +24 -0
  15. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/api/main.py +85 -17
  16. arc_devkit-0.8.0/arc_devkit/api/rate_limit.py +7 -0
  17. arc_devkit-0.8.0/arc_devkit/api/routes/agent_economy.py +195 -0
  18. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/api/routes/agents.py +34 -9
  19. arc_devkit-0.8.0/arc_devkit/api/routes/bridge.py +93 -0
  20. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/api/routes/copilot.py +43 -4
  21. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/api/routes/debugger.py +13 -2
  22. arc_devkit-0.8.0/arc_devkit/api/routes/fees.py +49 -0
  23. arc_devkit-0.8.0/arc_devkit/bridge/__init__.py +14 -0
  24. arc_devkit-0.8.0/arc_devkit/bridge/cctp.py +354 -0
  25. arc_devkit-0.8.0/arc_devkit/bridge/models.py +78 -0
  26. arc_devkit-0.8.0/arc_devkit/bridge/store.py +47 -0
  27. arc_devkit-0.8.0/arc_devkit/cli/commands/agent.py +542 -0
  28. arc_devkit-0.8.0/arc_devkit/cli/commands/bridge.py +148 -0
  29. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/cli/commands/copilot.py +44 -0
  30. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/cli/commands/debug.py +80 -0
  31. arc_devkit-0.8.0/arc_devkit/cli/commands/fees.py +55 -0
  32. arc_devkit-0.8.0/arc_devkit/cli/commands/mcp.py +31 -0
  33. arc_devkit-0.8.0/arc_devkit/cli/commands/network.py +130 -0
  34. arc_devkit-0.8.0/arc_devkit/cli/commands/oracle.py +43 -0
  35. arc_devkit-0.8.0/arc_devkit/cli/commands/privacy.py +56 -0
  36. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/cli/flat.py +72 -0
  37. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/cli/main.py +10 -1
  38. arc_devkit-0.8.0/arc_devkit/config.py +152 -0
  39. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/contracts/loader.py +4 -2
  40. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/copilot/agent.py +137 -8
  41. arc_devkit-0.8.0/arc_devkit/copilot/tools.py +355 -0
  42. arc_devkit-0.8.0/arc_devkit/core/gas.py +157 -0
  43. arc_devkit-0.8.0/arc_devkit/core/signer.py +121 -0
  44. arc_devkit-0.8.0/arc_devkit/core/validation.py +114 -0
  45. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/debugger/tx_analyzer.py +61 -10
  46. arc_devkit-0.8.0/arc_devkit/mcp_server.py +116 -0
  47. arc_devkit-0.8.0/arc_devkit/networks.py +67 -0
  48. arc_devkit-0.8.0/arc_devkit/oracle/__init__.py +5 -0
  49. arc_devkit-0.8.0/arc_devkit/oracle/price_feed.py +86 -0
  50. arc_devkit-0.8.0/arc_devkit/paymaster/__init__.py +16 -0
  51. arc_devkit-0.8.0/arc_devkit/paymaster/detector.py +42 -0
  52. arc_devkit-0.8.0/arc_devkit/paymaster/user_operation.py +92 -0
  53. arc_devkit-0.8.0/arc_devkit/privacy/__init__.py +23 -0
  54. arc_devkit-0.8.0/arc_devkit/privacy/confidential_transfer.py +86 -0
  55. arc_devkit-0.8.0/arc_devkit/privacy/view_key.py +100 -0
  56. arc_devkit-0.8.0/arc_devkit/stablecoins/__init__.py +5 -0
  57. {arc_devkit-0.4.6/arc_devkit/usdc → arc_devkit-0.8.0/arc_devkit/stablecoins}/token.py +49 -24
  58. arc_devkit-0.8.0/arc_devkit/usdc/token.py +18 -0
  59. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit.egg-info/PKG-INFO +10 -1
  60. arc_devkit-0.8.0/arc_devkit.egg-info/SOURCES.txt +126 -0
  61. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit.egg-info/requires.txt +11 -0
  62. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/pyproject.toml +18 -1
  63. arc_devkit-0.8.0/tests/test_agent_identity.py +167 -0
  64. arc_devkit-0.8.0/tests/test_agent_jobs.py +196 -0
  65. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_agents.py +73 -0
  66. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_analytics.py +74 -2
  67. arc_devkit-0.8.0/tests/test_api_agent_economy.py +168 -0
  68. arc_devkit-0.8.0/tests/test_api_bridge.py +70 -0
  69. arc_devkit-0.8.0/tests/test_api_fees.py +61 -0
  70. arc_devkit-0.8.0/tests/test_api_security.py +134 -0
  71. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_async_agents.py +37 -0
  72. arc_devkit-0.8.0/tests/test_autonomous.py +299 -0
  73. arc_devkit-0.8.0/tests/test_bridge.py +448 -0
  74. arc_devkit-0.8.0/tests/test_cli_agent_economy.py +174 -0
  75. arc_devkit-0.8.0/tests/test_cli_bridge.py +106 -0
  76. arc_devkit-0.8.0/tests/test_cli_debug_advanced.py +79 -0
  77. arc_devkit-0.8.0/tests/test_cli_fees.py +31 -0
  78. arc_devkit-0.8.0/tests/test_cli_network.py +34 -0
  79. arc_devkit-0.8.0/tests/test_cli_sprint6.py +80 -0
  80. arc_devkit-0.8.0/tests/test_config.py +69 -0
  81. arc_devkit-0.8.0/tests/test_connection_regression.py +34 -0
  82. arc_devkit-0.8.0/tests/test_copilot_tools.py +258 -0
  83. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_debugger.py +45 -0
  84. arc_devkit-0.8.0/tests/test_gas.py +88 -0
  85. arc_devkit-0.8.0/tests/test_guardrails.py +90 -0
  86. arc_devkit-0.8.0/tests/test_job_agent.py +100 -0
  87. arc_devkit-0.8.0/tests/test_mcp_server.py +119 -0
  88. arc_devkit-0.8.0/tests/test_oracle.py +56 -0
  89. arc_devkit-0.8.0/tests/test_paymaster.py +89 -0
  90. arc_devkit-0.8.0/tests/test_payment_security.py +188 -0
  91. arc_devkit-0.8.0/tests/test_privacy.py +118 -0
  92. arc_devkit-0.8.0/tests/test_signer.py +78 -0
  93. arc_devkit-0.8.0/tests/test_stablecoins.py +111 -0
  94. arc_devkit-0.8.0/tests/test_validation.py +96 -0
  95. arc_devkit-0.4.6/arc_devkit/cli/commands/agent.py +0 -178
  96. arc_devkit-0.4.6/arc_devkit/config.py +0 -66
  97. arc_devkit-0.4.6/arc_devkit/core/gas.py +0 -64
  98. arc_devkit-0.4.6/arc_devkit.egg-info/SOURCES.txt +0 -60
  99. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/README.md +0 -0
  100. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/__init__.py +0 -0
  101. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/agents/__init__.py +0 -0
  102. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/agents/async_base.py +0 -0
  103. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/agents/async_monitor.py +0 -0
  104. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/analytics/__init__.py +0 -0
  105. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/api/__init__.py +0 -0
  106. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/api/routes/__init__.py +0 -0
  107. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/cli/__init__.py +0 -0
  108. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/cli/commands/__init__.py +0 -0
  109. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/contracts/__init__.py +0 -0
  110. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/copilot/__init__.py +0 -0
  111. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/core/__init__.py +0 -0
  112. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/core/connection.py +0 -0
  113. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/core/wallet.py +0 -0
  114. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/debugger/__init__.py +0 -0
  115. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/deploy/__init__.py +0 -0
  116. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/deploy/deployer.py +0 -0
  117. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/events/__init__.py +0 -0
  118. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/events/listener.py +0 -0
  119. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit/usdc/__init__.py +0 -0
  120. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit.egg-info/dependency_links.txt +0 -0
  121. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit.egg-info/entry_points.txt +0 -0
  122. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/arc_devkit.egg-info/top_level.txt +0 -0
  123. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/setup.cfg +0 -0
  124. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_api.py +0 -0
  125. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_cli.py +0 -0
  126. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_connection.py +0 -0
  127. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_contracts.py +0 -0
  128. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_copilot.py +0 -0
  129. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_deploy.py +0 -0
  130. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_events.py +0 -0
  131. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_integration.py +0 -0
  132. {arc_devkit-0.4.6 → arc_devkit-0.8.0}/tests/test_usdc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arc-devkit
3
- Version: 0.4.6
3
+ Version: 0.8.0
4
4
  Summary: Plataforma open source de ferramentas para desenvolvedores da Arc blockchain (Circle)
5
5
  Author-email: Jeielsantosdev <jeielsantos.ti@gmail.com>
6
6
  License-Expression: MIT
@@ -35,7 +35,16 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
35
35
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
36
36
  Requires-Dist: ruff>=0.6; extra == "dev"
37
37
  Requires-Dist: mypy>=1.8; extra == "dev"
38
+ Requires-Dist: bandit>=1.7; extra == "dev"
39
+ Requires-Dist: pip-audit>=2.7; extra == "dev"
38
40
  Requires-Dist: httpx>=0.27; extra == "dev"
41
+ Requires-Dist: vcrpy>=6.0; extra == "dev"
42
+ Requires-Dist: mcp>=1.2; extra == "dev"
43
+ Requires-Dist: locust>=2.29; extra == "dev"
44
+ Provides-Extra: security
45
+ Requires-Dist: keyring>=24; extra == "security"
46
+ Provides-Extra: mcp
47
+ Requires-Dist: mcp>=1.2; extra == "mcp"
39
48
 
40
49
  # Arc DevKit
41
50
 
@@ -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
+ )
@@ -74,6 +74,14 @@ class BaseAgent(ABC):
74
74
  account = Account.from_key(_key)
75
75
  self._address: str | None = account.address
76
76
  self._private_key: str | None = _key
77
+
78
+ from arc_devkit.core.signer import LocalKeySigner
79
+
80
+ # Prepares the pluggable Signer interface (core/signer.py) for
81
+ # future hardware-wallet/post-quantum backends — subclasses still
82
+ # sign via self._private_key today; this is additive, not a
83
+ # behavior change.
84
+ self._signer: LocalKeySigner | None = LocalKeySigner(_key)
77
85
  logger.info(
78
86
  "[%s] Initialized with wallet %s",
79
87
  self.__class__.__name__,
@@ -82,6 +90,7 @@ class BaseAgent(ABC):
82
90
  else:
83
91
  self._address = None
84
92
  self._private_key = None
93
+ self._signer = None
85
94
  logger.warning(
86
95
  "[%s] No private key — read-only mode.",
87
96
  self.__class__.__name__,
@@ -0,0 +1,167 @@
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.jobs import Job, JobRegistry
14
+ from arc_devkit.agents.monitor_agent import MonitorAgent
15
+ from arc_devkit.agents.payment_agent import PaymentAgent
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _PLAN_PROMPT = """\
20
+ You are coordinating economic agents on the Arc blockchain. Analyze the goal
21
+ below using your read-only tools (balances, gas, block info) and produce a plan.
22
+
23
+ Goal: {goal}
24
+
25
+ Respond with:
26
+ 1. **Current state** — relevant on-chain data you gathered
27
+ 2. **Plan** — numbered steps; mark each step as [read-only] or [requires-funds]
28
+ 3. **Risks** — what could go wrong and which guardrails apply
29
+ """
30
+
31
+
32
+ class CoordinatorAgent:
33
+ """
34
+ Orchestrates DevCopilot, PaymentAgent, and MonitorAgent around a goal.
35
+
36
+ Example:
37
+ coordinator = CoordinatorAgent(guardrails=Guardrails.from_settings())
38
+ coordinator.register_payment_agent(PaymentAgent(guardrails=guardrails))
39
+ result = coordinator.plan("keep 0xAbc... above 0.5 USDC")
40
+ """
41
+
42
+ def __init__(self, guardrails: Guardrails | None = None, bus: EventBus | None = None) -> None:
43
+ self._guardrails = guardrails or Guardrails.from_settings()
44
+ self.bus = bus or EventBus()
45
+ self._payment_agent: PaymentAgent | None = None
46
+ self._monitors: list[MonitorAgent] = []
47
+
48
+ def register_payment_agent(self, agent: PaymentAgent) -> None:
49
+ """Register the payment agent used for fund-moving steps (guardrails enforced)."""
50
+ if agent._guardrails is None:
51
+ agent._guardrails = self._guardrails
52
+ self._payment_agent = agent
53
+
54
+ def register_monitor(self, monitor: MonitorAgent) -> None:
55
+ """Register a monitor whose events feed the coordinator's bus."""
56
+ self._monitors.append(monitor)
57
+
58
+ def plan(self, goal: str, max_iterations: int = 10) -> dict:
59
+ """
60
+ Produce an execution plan for a natural-language goal.
61
+
62
+ Uses DevCopilot agentic mode with read-only tools; no funds are moved.
63
+
64
+ Returns:
65
+ Dict with 'response' (the plan), 'tool_calls', and 'iterations'.
66
+ """
67
+ from arc_devkit.copilot.agent import DevCopilot
68
+
69
+ self._guardrails.check_kill_switch()
70
+ copilot = DevCopilot()
71
+ result = copilot.run_agent(_PLAN_PROMPT.format(goal=goal), max_iterations=max_iterations)
72
+ self.bus.publish_sync("coordinator.plan", {"goal": goal, "plan": result["response"]})
73
+ self._guardrails.audit(
74
+ agent="CoordinatorAgent",
75
+ trigger="plan",
76
+ action="generate_plan",
77
+ goal=goal,
78
+ tool_calls=len(result["tool_calls"]),
79
+ )
80
+ return result
81
+
82
+ def execute_transfer(self, to: str, amount_usdc: float, trigger: str = "coordinator") -> dict:
83
+ """
84
+ Execute a fund-moving step through the registered (guarded) PaymentAgent.
85
+
86
+ Returns:
87
+ The PaymentAgent result dict; status "blocked" when a guardrail denies it.
88
+ """
89
+ if self._payment_agent is None:
90
+ return {"status": "error", "error": "No PaymentAgent registered."}
91
+ result = self._payment_agent.execute(
92
+ to=to, amount_usdc=amount_usdc, enviar=True, trigger=trigger
93
+ )
94
+ self.bus.publish_sync("coordinator.transfer", {"to": to, "result": result})
95
+ return result
96
+
97
+ def hire_agent(
98
+ self,
99
+ job_registry: JobRegistry,
100
+ agent_address: str,
101
+ amount_usdc: float,
102
+ spec: str,
103
+ ) -> Job:
104
+ """
105
+ Create a job (with USDC escrow) hiring another agent — planning a
106
+ workflow that contracts other agents via ERC-8183 jobs.
107
+
108
+ Requires a PaymentAgent registered with a private key (its wallet
109
+ signs the job-creation tx and funds the escrow). The caller must have
110
+ already approved `job_registry`'s contract to spend `amount_usdc` USDC.
111
+ """
112
+ from decimal import Decimal
113
+
114
+ if self._payment_agent is None or self._payment_agent._private_key is None:
115
+ return Job(
116
+ job_id=0,
117
+ requester="",
118
+ agent=agent_address,
119
+ amount_usdc=Decimal(str(amount_usdc)),
120
+ spec=spec,
121
+ error="No PaymentAgent with a private key registered.",
122
+ )
123
+
124
+ job = job_registry.create_job(
125
+ agent_address,
126
+ Decimal(str(amount_usdc)),
127
+ spec,
128
+ self._payment_agent._private_key,
129
+ )
130
+ self.bus.publish_sync(
131
+ "coordinator.hire", {"agent": agent_address, "job_id": job.job_id, "spec": spec}
132
+ )
133
+ return job
134
+
135
+ def maintain_balance(
136
+ self,
137
+ address: str,
138
+ min_usdc: float,
139
+ top_up_usdc: float,
140
+ interval_seconds: int = 30,
141
+ max_iterations: int = 0,
142
+ ) -> dict:
143
+ """
144
+ Declarative workflow: keep an address above a minimum balance.
145
+
146
+ Wires an AutoRefueler to a MonitorAgent and runs the monitoring loop
147
+ (blocking). Requires a registered PaymentAgent with funds.
148
+ """
149
+ from decimal import Decimal
150
+
151
+ from arc_devkit.agents.autonomous import AutoRefueler
152
+
153
+ if self._payment_agent is None:
154
+ return {"status": "error", "error": "No PaymentAgent registered."}
155
+
156
+ monitor = MonitorAgent(watched_address=address, interval_seconds=interval_seconds)
157
+ refueler = AutoRefueler(
158
+ target_address=address,
159
+ threshold_usdc=Decimal(str(min_usdc)),
160
+ top_up_amount_usdc=Decimal(str(top_up_usdc)),
161
+ payment_agent=self._payment_agent,
162
+ guardrails=self._guardrails,
163
+ )
164
+ refueler.attach(monitor)
165
+ self.register_monitor(monitor)
166
+ logger.info("maintain_balance workflow started for %s", address)
167
+ 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)