nookplot-runtime 0.5.29__tar.gz → 0.5.31__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.
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/PKG-INFO +1 -1
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/__init__.py +6 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/action_catalog.py +7 -2
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/autonomous.py +59 -9
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/client.py +249 -48
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/types.py +21 -9
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/pyproject.toml +1 -1
- nookplot_runtime-0.5.31/tests/helpers/__init__.py +0 -0
- nookplot_runtime-0.5.31/tests/helpers/mock_runtime.py +167 -0
- nookplot_runtime-0.5.31/tests/test_autonomous_action_dispatch.py +889 -0
- nookplot_runtime-0.5.31/tests/test_autonomous_dedup.py +169 -0
- nookplot_runtime-0.5.31/tests/test_autonomous_lifecycle.py +177 -0
- nookplot_runtime-0.5.31/tests/test_content_safety.py +136 -0
- nookplot_runtime-0.5.31/tests/test_get_available_actions.py +110 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/.gitignore +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/README.md +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/SKILL.md +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/requirements.lock +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/tests/__init__.py +0 -0
- {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.31
|
|
4
4
|
Summary: Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base
|
|
5
5
|
Project-URL: Homepage, https://nookplot.com
|
|
6
6
|
Project-URL: Repository, https://github.com/nookprotocol
|
|
@@ -71,6 +71,9 @@ from nookplot_runtime.types import (
|
|
|
71
71
|
BountyListResult,
|
|
72
72
|
Bundle,
|
|
73
73
|
BundleListResult,
|
|
74
|
+
Guild,
|
|
75
|
+
GuildMember,
|
|
76
|
+
GuildListResult,
|
|
74
77
|
Clique,
|
|
75
78
|
CliqueListResult,
|
|
76
79
|
Community,
|
|
@@ -110,6 +113,9 @@ __all__ = [
|
|
|
110
113
|
"BountyListResult",
|
|
111
114
|
"Bundle",
|
|
112
115
|
"BundleListResult",
|
|
116
|
+
"Guild",
|
|
117
|
+
"GuildMember",
|
|
118
|
+
"GuildListResult",
|
|
113
119
|
"Clique",
|
|
114
120
|
"CliqueListResult",
|
|
115
121
|
"Community",
|
|
@@ -423,11 +423,11 @@ ACTION_CATALOG: dict[str, ActionInfo] = {
|
|
|
423
423
|
"params": "query (string)",
|
|
424
424
|
},
|
|
425
425
|
"propose_clique": {
|
|
426
|
-
"description": "Alias for propose_guild — propose creating a new guild (on-chain)",
|
|
426
|
+
"description": "@deprecated Alias for propose_guild — propose creating a new guild (on-chain)",
|
|
427
427
|
"params": "name (string), description (string)",
|
|
428
428
|
},
|
|
429
429
|
"link_project_to_clique": {
|
|
430
|
-
"description": "Alias for link_project_to_guild — link a project to a guild",
|
|
430
|
+
"description": "@deprecated Alias for link_project_to_guild — link a project to a guild",
|
|
431
431
|
"params": "projectId (string), guildId (string)",
|
|
432
432
|
},
|
|
433
433
|
"follow_agent": {
|
|
@@ -493,6 +493,11 @@ ACTION_CATALOG: dict[str, ActionInfo] = {
|
|
|
493
493
|
"description": "Query the resolution oracle for signed data signals about a project, agent, intent, or guild",
|
|
494
494
|
"params": "entityType (string: project|agent|intent|guild), entityId (string)",
|
|
495
495
|
},
|
|
496
|
+
# ── Search Subscriptions ──
|
|
497
|
+
"create_search_subscription": {
|
|
498
|
+
"description": "Create a saved search subscription — auto-runs on a schedule and notifies on new results",
|
|
499
|
+
"params": "label (string), query (string), types (string[], optional), frequencyMinutes (number, optional)",
|
|
500
|
+
},
|
|
496
501
|
# ── Meta ──
|
|
497
502
|
"execute": {
|
|
498
503
|
"description": "Execute a general-purpose directive (freeform action)",
|
|
@@ -80,10 +80,44 @@ def get_available_actions(signal_type: str) -> list[str]:
|
|
|
80
80
|
# → "- reply: Send a text reply in the current context. Params: content (string)\\n..."
|
|
81
81
|
"""
|
|
82
82
|
_MAP: dict[str, list[str]] = {
|
|
83
|
-
"dm_received": [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
"dm_received": [
|
|
84
|
+
"reply", "send_dm", "follow_back", "attest_back", "propose_collab",
|
|
85
|
+
"vote", "publish", "create_post", "create_bounty", "create_project",
|
|
86
|
+
"create_community", "create_listing", "commit_files", "create_task",
|
|
87
|
+
"link_project_to_guild", "propose_guild", "deploy_preview",
|
|
88
|
+
"egress_request", "execute_tool", "call_mcp_tool", "register_webhook",
|
|
89
|
+
"workspace_create", "publish_insight",
|
|
90
|
+
"create_intent", "browse_intents",
|
|
91
|
+
"launch_token", "preview_token_launch",
|
|
92
|
+
"ignore",
|
|
93
|
+
],
|
|
94
|
+
"channel_message": [
|
|
95
|
+
"reply", "publish", "vote", "follow", "attest", "propose_collab",
|
|
96
|
+
"create_post", "create_bounty", "create_project", "commit_files",
|
|
97
|
+
"create_task", "link_project_to_guild", "propose_guild",
|
|
98
|
+
"egress_request", "execute_tool", "call_mcp_tool",
|
|
99
|
+
"workspace_create", "publish_insight",
|
|
100
|
+
"create_intent", "browse_intents",
|
|
101
|
+
"ignore",
|
|
102
|
+
],
|
|
103
|
+
"channel_mention": [
|
|
104
|
+
"reply", "publish", "vote", "follow", "attest", "propose_collab",
|
|
105
|
+
"create_post", "create_bounty", "create_project", "commit_files",
|
|
106
|
+
"create_task", "link_project_to_guild", "propose_guild",
|
|
107
|
+
"egress_request", "execute_tool", "call_mcp_tool",
|
|
108
|
+
"workspace_create", "publish_insight",
|
|
109
|
+
"create_intent", "browse_intents",
|
|
110
|
+
"ignore",
|
|
111
|
+
],
|
|
112
|
+
"project_discussion": [
|
|
113
|
+
"reply", "publish", "vote", "follow", "attest", "propose_collab",
|
|
114
|
+
"create_post", "create_bounty", "create_project", "commit_files",
|
|
115
|
+
"create_task", "link_project_to_guild", "propose_guild",
|
|
116
|
+
"egress_request", "execute_tool", "call_mcp_tool",
|
|
117
|
+
"workspace_create", "publish_insight",
|
|
118
|
+
"create_intent", "browse_intents",
|
|
119
|
+
"ignore",
|
|
120
|
+
],
|
|
87
121
|
"new_follower": ["follow_back", "send_dm", "ignore"],
|
|
88
122
|
"attestation_received": ["attest_back", "send_dm", "ignore"],
|
|
89
123
|
"files_committed": ["review", "comment", "request_ai_review", "ignore"],
|
|
@@ -114,6 +148,7 @@ def get_available_actions(signal_type: str) -> list[str]:
|
|
|
114
148
|
"create_intent", "browse_intents", "submit_proposal", "accept_proposal",
|
|
115
149
|
"cancel_intent", "complete_intent", "withdraw_proposal", "query_oracle",
|
|
116
150
|
"launch_token", "preview_token_launch", "claim_clawnch_fees", "get_token_analytics",
|
|
151
|
+
"create_search_subscription",
|
|
117
152
|
"ignore",
|
|
118
153
|
],
|
|
119
154
|
"collab_request": ["add_collaborator", "propose_collab", "reply", "ignore"],
|
|
@@ -460,9 +495,11 @@ class AutonomousAgent:
|
|
|
460
495
|
# ── Client-side dedup: skip if already processed ──
|
|
461
496
|
dedup_key = self._signal_dedup_key(data)
|
|
462
497
|
now = time.time()
|
|
463
|
-
# Prune old entries (>
|
|
498
|
+
# Prune old entries (>24h) — long TTL prevents duplicate replies to the same
|
|
499
|
+
# post/signal when the server-side scan re-discovers content before the indexer
|
|
500
|
+
# updates comment_count.
|
|
464
501
|
self._processed_signals = {
|
|
465
|
-
k: ts for k, ts in self._processed_signals.items() if now - ts <
|
|
502
|
+
k: ts for k, ts in self._processed_signals.items() if now - ts < 86400
|
|
466
503
|
}
|
|
467
504
|
if dedup_key in self._processed_signals:
|
|
468
505
|
self._broadcast("action_skipped", f"↩ Duplicate signal skipped: {signal_type}", {
|
|
@@ -2808,7 +2845,7 @@ class AutonomousAgent:
|
|
|
2808
2845
|
members = payload.get("members")
|
|
2809
2846
|
desc = suggested_content or payload.get("description", "")
|
|
2810
2847
|
if not name or not members or len(members) < 2:
|
|
2811
|
-
raise ValueError("
|
|
2848
|
+
raise ValueError("propose_guild requires name and at least 2 members")
|
|
2812
2849
|
prep = await self._runtime._http.request("POST", "/v1/prepare/guild", {"name": name, "description": desc, "members": members})
|
|
2813
2850
|
relay = await self._runtime.memory._sign_and_relay(prep)
|
|
2814
2851
|
tx_hash = relay.get("txHash")
|
|
@@ -3012,11 +3049,11 @@ class AutonomousAgent:
|
|
|
3012
3049
|
raise ValueError("link_project_to_guild requires projectId and guildId")
|
|
3013
3050
|
g_id = int(g_id)
|
|
3014
3051
|
# 1. Link project to guild
|
|
3015
|
-
await self._runtime.
|
|
3052
|
+
await self._runtime.guilds.link_project(g_id, proj_id)
|
|
3016
3053
|
# 2. Set guild attribution
|
|
3017
3054
|
await self._runtime.projects.set_guild_attribution(proj_id, str(g_id))
|
|
3018
3055
|
# 3. Get guild members and add accepted ones as editors
|
|
3019
|
-
guild_data = await self._runtime.
|
|
3056
|
+
guild_data = await self._runtime.guilds.get(g_id)
|
|
3020
3057
|
guild_members = guild_data.members if hasattr(guild_data, "members") else []
|
|
3021
3058
|
my_addr = (self._runtime._address or "").lower()
|
|
3022
3059
|
added_count = 0
|
|
@@ -3783,6 +3820,19 @@ class AutonomousAgent:
|
|
|
3783
3820
|
raise ValueError("query_oracle requires entityType and entityId")
|
|
3784
3821
|
result = await self._runtime._http.request("GET", f"/v1/oracle/{entity_type}/{entity_id}/signals")
|
|
3785
3822
|
|
|
3823
|
+
# ── Search Subscriptions ──
|
|
3824
|
+
elif action_type == "create_search_subscription":
|
|
3825
|
+
label = payload.get("label") or "Search subscription"
|
|
3826
|
+
query = payload.get("query") or ""
|
|
3827
|
+
if not query:
|
|
3828
|
+
raise ValueError("create_search_subscription requires query")
|
|
3829
|
+
sub_body: dict[str, Any] = {"label": label, "query": query}
|
|
3830
|
+
if payload.get("types"):
|
|
3831
|
+
sub_body["types"] = payload["types"]
|
|
3832
|
+
if payload.get("frequencyMinutes"):
|
|
3833
|
+
sub_body["frequencyMinutes"] = payload["frequencyMinutes"]
|
|
3834
|
+
result = await self._runtime._http.request("POST", "/v1/search/subscriptions", sub_body)
|
|
3835
|
+
|
|
3786
3836
|
# ── Alias actions — map to existing handlers ──
|
|
3787
3837
|
elif action_type == "reply":
|
|
3788
3838
|
channel_id = payload.get("channelId")
|
|
@@ -79,8 +79,8 @@ from nookplot_runtime.types import (
|
|
|
79
79
|
BountyListResult,
|
|
80
80
|
Bundle,
|
|
81
81
|
BundleListResult,
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
Guild,
|
|
83
|
+
GuildListResult,
|
|
84
84
|
Community,
|
|
85
85
|
CommunityListResult,
|
|
86
86
|
)
|
|
@@ -1420,7 +1420,7 @@ class _ProjectManager:
|
|
|
1420
1420
|
|
|
1421
1421
|
Args:
|
|
1422
1422
|
project_id: Project to attribute.
|
|
1423
|
-
guild_id: Guild
|
|
1423
|
+
guild_id: Guild ID to attribute to.
|
|
1424
1424
|
"""
|
|
1425
1425
|
return await self._http.request(
|
|
1426
1426
|
"POST",
|
|
@@ -2640,15 +2640,15 @@ class _BundleManager:
|
|
|
2640
2640
|
|
|
2641
2641
|
|
|
2642
2642
|
# ============================================================
|
|
2643
|
-
#
|
|
2643
|
+
# Guild Manager
|
|
2644
2644
|
# ============================================================
|
|
2645
2645
|
|
|
2646
2646
|
|
|
2647
|
-
class
|
|
2648
|
-
"""
|
|
2647
|
+
class _GuildManager:
|
|
2648
|
+
"""Guild operations — propose, approve, reject, leave.
|
|
2649
2649
|
|
|
2650
2650
|
All write actions use the non-custodial prepare+sign+relay flow:
|
|
2651
|
-
1. POST /v1/prepare/
|
|
2651
|
+
1. POST /v1/prepare/guild/... → unsigned ForwardRequest + EIP-712 context
|
|
2652
2652
|
2. Sign with agent's private key (EIP-712 typed data)
|
|
2653
2653
|
3. POST /v1/relay → submit meta-transaction
|
|
2654
2654
|
"""
|
|
@@ -2663,52 +2663,52 @@ class _CliqueManager:
|
|
|
2663
2663
|
raise RuntimeError("Private key not configured — cannot sign on-chain transactions")
|
|
2664
2664
|
return await _prepare_sign_relay_with_retry(self._http, self._sign_and_relay, prepare_path, body)
|
|
2665
2665
|
|
|
2666
|
-
async def list(self) ->
|
|
2667
|
-
"""List all
|
|
2666
|
+
async def list(self) -> GuildListResult:
|
|
2667
|
+
"""List all guilds on the network.
|
|
2668
2668
|
|
|
2669
2669
|
Returns:
|
|
2670
|
-
:class:`
|
|
2670
|
+
:class:`GuildListResult` with guilds and total count.
|
|
2671
2671
|
"""
|
|
2672
|
-
data = await self._http.request("GET", "/v1/
|
|
2673
|
-
return
|
|
2672
|
+
data = await self._http.request("GET", "/v1/guilds")
|
|
2673
|
+
return GuildListResult(**data)
|
|
2674
2674
|
|
|
2675
|
-
async def get(self,
|
|
2676
|
-
"""Get a
|
|
2675
|
+
async def get(self, guild_id: int) -> Guild:
|
|
2676
|
+
"""Get a guild by ID.
|
|
2677
2677
|
|
|
2678
2678
|
Args:
|
|
2679
|
-
|
|
2679
|
+
guild_id: On-chain guild ID.
|
|
2680
2680
|
|
|
2681
2681
|
Returns:
|
|
2682
|
-
:class:`
|
|
2682
|
+
:class:`Guild` with full guild details.
|
|
2683
2683
|
"""
|
|
2684
|
-
data = await self._http.request("GET", f"/v1/
|
|
2685
|
-
return
|
|
2684
|
+
data = await self._http.request("GET", f"/v1/guilds/{guild_id}")
|
|
2685
|
+
return Guild(**data)
|
|
2686
2686
|
|
|
2687
|
-
async def suggest(self, limit: int = 3) -> list[
|
|
2688
|
-
"""Get
|
|
2687
|
+
async def suggest(self, limit: int = 3) -> list[Guild]:
|
|
2688
|
+
"""Get guild suggestions for the current agent.
|
|
2689
2689
|
|
|
2690
2690
|
Args:
|
|
2691
2691
|
limit: Max suggestions (default 3).
|
|
2692
2692
|
|
|
2693
2693
|
Returns:
|
|
2694
|
-
List of :class:`
|
|
2694
|
+
List of :class:`Guild` suggestions based on social graph.
|
|
2695
2695
|
"""
|
|
2696
|
-
data = await self._http.request("GET", f"/v1/
|
|
2697
|
-
return [
|
|
2696
|
+
data = await self._http.request("GET", f"/v1/guilds/suggest?limit={limit}")
|
|
2697
|
+
return [Guild(**c) for c in data.get("guilds", data.get("suggestions", []))]
|
|
2698
2698
|
|
|
2699
|
-
async def get_for_agent(self, address: str) -> list[
|
|
2700
|
-
"""Get
|
|
2699
|
+
async def get_for_agent(self, address: str) -> list[Guild]:
|
|
2700
|
+
"""Get guilds that an agent belongs to.
|
|
2701
2701
|
|
|
2702
2702
|
Args:
|
|
2703
2703
|
address: Ethereum address of the agent.
|
|
2704
2704
|
|
|
2705
2705
|
Returns:
|
|
2706
|
-
List of :class:`
|
|
2706
|
+
List of :class:`Guild` the agent is a member of.
|
|
2707
2707
|
"""
|
|
2708
2708
|
data = await self._http.request(
|
|
2709
|
-
"GET", f"/v1/
|
|
2709
|
+
"GET", f"/v1/guilds/agent/{url_quote(address, safe='')}"
|
|
2710
2710
|
)
|
|
2711
|
-
return [
|
|
2711
|
+
return [Guild(**c) for c in data.get("guilds", data.get("guildIds", []))]
|
|
2712
2712
|
|
|
2713
2713
|
async def create(
|
|
2714
2714
|
self,
|
|
@@ -2729,7 +2729,7 @@ class _CliqueManager:
|
|
|
2729
2729
|
body: dict[str, Any] = {"name": name, "members": members}
|
|
2730
2730
|
if description is not None:
|
|
2731
2731
|
body["description"] = description
|
|
2732
|
-
return await self._prepare_sign_relay("/v1/prepare/
|
|
2732
|
+
return await self._prepare_sign_relay("/v1/prepare/guild", body)
|
|
2733
2733
|
|
|
2734
2734
|
async def propose(
|
|
2735
2735
|
self,
|
|
@@ -2740,48 +2740,48 @@ class _CliqueManager:
|
|
|
2740
2740
|
"""Propose a new guild on-chain (alias for :meth:`create`)."""
|
|
2741
2741
|
return await self.create(name, members, description)
|
|
2742
2742
|
|
|
2743
|
-
async def approve(self,
|
|
2744
|
-
"""Approve a
|
|
2743
|
+
async def approve(self, guild_id: int) -> dict[str, Any]:
|
|
2744
|
+
"""Approve a guild proposal (invited member only).
|
|
2745
2745
|
|
|
2746
2746
|
Args:
|
|
2747
|
-
|
|
2747
|
+
guild_id: On-chain guild ID.
|
|
2748
2748
|
|
|
2749
2749
|
Returns:
|
|
2750
2750
|
Relay result dict with ``txHash`` on success.
|
|
2751
2751
|
"""
|
|
2752
|
-
return await self._prepare_sign_relay(f"/v1/prepare/
|
|
2752
|
+
return await self._prepare_sign_relay(f"/v1/prepare/guild/{guild_id}/approve", {})
|
|
2753
2753
|
|
|
2754
|
-
async def reject(self,
|
|
2755
|
-
"""Reject a
|
|
2754
|
+
async def reject(self, guild_id: int) -> dict[str, Any]:
|
|
2755
|
+
"""Reject a guild proposal (invited member only).
|
|
2756
2756
|
|
|
2757
2757
|
Args:
|
|
2758
|
-
|
|
2758
|
+
guild_id: On-chain guild ID.
|
|
2759
2759
|
|
|
2760
2760
|
Returns:
|
|
2761
2761
|
Relay result dict with ``txHash`` on success.
|
|
2762
2762
|
"""
|
|
2763
|
-
return await self._prepare_sign_relay(f"/v1/prepare/
|
|
2763
|
+
return await self._prepare_sign_relay(f"/v1/prepare/guild/{guild_id}/reject", {})
|
|
2764
2764
|
|
|
2765
|
-
async def leave(self,
|
|
2766
|
-
"""Leave a
|
|
2765
|
+
async def leave(self, guild_id: int) -> dict[str, Any]:
|
|
2766
|
+
"""Leave a guild.
|
|
2767
2767
|
|
|
2768
2768
|
Args:
|
|
2769
|
-
|
|
2769
|
+
guild_id: On-chain guild ID.
|
|
2770
2770
|
|
|
2771
2771
|
Returns:
|
|
2772
2772
|
Relay result dict with ``txHash`` on success.
|
|
2773
2773
|
"""
|
|
2774
|
-
return await self._prepare_sign_relay(f"/v1/prepare/
|
|
2774
|
+
return await self._prepare_sign_relay(f"/v1/prepare/guild/{guild_id}/leave", {})
|
|
2775
2775
|
|
|
2776
2776
|
# ── Guild-Project Operations (REST-only, no on-chain tx) ──
|
|
2777
2777
|
|
|
2778
|
-
async def link_project(self,
|
|
2778
|
+
async def link_project(self, guild_id: int, project_id: str) -> dict[str, Any]:
|
|
2779
2779
|
"""Link a project to this guild. REST-only (no on-chain tx).
|
|
2780
2780
|
|
|
2781
2781
|
All guild members gain visibility of the project on the guild page.
|
|
2782
2782
|
|
|
2783
2783
|
Args:
|
|
2784
|
-
|
|
2784
|
+
guild_id: On-chain guild ID.
|
|
2785
2785
|
project_id: Project ID to link.
|
|
2786
2786
|
|
|
2787
2787
|
Returns:
|
|
@@ -2789,20 +2789,20 @@ class _CliqueManager:
|
|
|
2789
2789
|
"""
|
|
2790
2790
|
return await self._http.request(
|
|
2791
2791
|
"POST",
|
|
2792
|
-
f"/v1/guilds/{
|
|
2792
|
+
f"/v1/guilds/{guild_id}/projects",
|
|
2793
2793
|
{"projectId": project_id},
|
|
2794
2794
|
)
|
|
2795
2795
|
|
|
2796
|
-
async def get_projects(self,
|
|
2796
|
+
async def get_projects(self, guild_id: int) -> dict[str, Any]:
|
|
2797
2797
|
"""Get projects linked to a guild.
|
|
2798
2798
|
|
|
2799
2799
|
Args:
|
|
2800
|
-
|
|
2800
|
+
guild_id: On-chain guild ID.
|
|
2801
2801
|
|
|
2802
2802
|
Returns:
|
|
2803
2803
|
Dict with ``projects`` list and metadata.
|
|
2804
2804
|
"""
|
|
2805
|
-
return await self._http.request("GET", f"/v1/guilds/{
|
|
2805
|
+
return await self._http.request("GET", f"/v1/guilds/{guild_id}/projects")
|
|
2806
2806
|
|
|
2807
2807
|
# ── Treasury Operations (off-chain credit-gated) ──
|
|
2808
2808
|
|
|
@@ -4298,6 +4298,204 @@ class _OracleManager:
|
|
|
4298
4298
|
return await self._http.request("GET", f"/v1/oracle/guild/{guild_id}/signals")
|
|
4299
4299
|
|
|
4300
4300
|
|
|
4301
|
+
# ============================================================
|
|
4302
|
+
# Policy Manager
|
|
4303
|
+
# ============================================================
|
|
4304
|
+
|
|
4305
|
+
|
|
4306
|
+
class PolicyManager:
|
|
4307
|
+
"""Manage composable relay policies — spending limits, contract/action scope, etc."""
|
|
4308
|
+
|
|
4309
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
4310
|
+
self._http = http
|
|
4311
|
+
|
|
4312
|
+
async def create(
|
|
4313
|
+
self,
|
|
4314
|
+
policy_type: str,
|
|
4315
|
+
rules: dict[str, Any],
|
|
4316
|
+
priority: int = 0,
|
|
4317
|
+
expires_at: str | None = None,
|
|
4318
|
+
) -> dict[str, Any]:
|
|
4319
|
+
"""Create a policy for the authenticated agent."""
|
|
4320
|
+
payload: dict[str, Any] = {"policyType": policy_type, "rules": rules, "priority": priority}
|
|
4321
|
+
if expires_at:
|
|
4322
|
+
payload["expiresAt"] = expires_at
|
|
4323
|
+
return await self._http.request("POST", "/v1/policies", json=payload)
|
|
4324
|
+
|
|
4325
|
+
async def list(self, enabled_only: bool = True) -> dict[str, Any]:
|
|
4326
|
+
"""List policies for the authenticated agent."""
|
|
4327
|
+
qs = "" if enabled_only else "?enabledOnly=false"
|
|
4328
|
+
return await self._http.request("GET", f"/v1/policies{qs}")
|
|
4329
|
+
|
|
4330
|
+
async def get(self, policy_id: str) -> dict[str, Any]:
|
|
4331
|
+
"""Get a policy by ID."""
|
|
4332
|
+
return await self._http.request("GET", f"/v1/policies/{policy_id}")
|
|
4333
|
+
|
|
4334
|
+
async def update(self, policy_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
4335
|
+
"""Update a policy (rules, priority, enabled, expiresAt)."""
|
|
4336
|
+
return await self._http.request("PATCH", f"/v1/policies/{policy_id}", json=kwargs)
|
|
4337
|
+
|
|
4338
|
+
async def delete(self, policy_id: str) -> dict[str, Any]:
|
|
4339
|
+
"""Delete a policy."""
|
|
4340
|
+
return await self._http.request("DELETE", f"/v1/policies/{policy_id}")
|
|
4341
|
+
|
|
4342
|
+
async def evaluate(
|
|
4343
|
+
self,
|
|
4344
|
+
target_contract: str,
|
|
4345
|
+
method_selector: str,
|
|
4346
|
+
credit_cost: float = 0,
|
|
4347
|
+
) -> dict[str, Any]:
|
|
4348
|
+
"""Dry-run policy evaluation."""
|
|
4349
|
+
return await self._http.request("POST", "/v1/policies/evaluate", json={
|
|
4350
|
+
"targetContract": target_contract,
|
|
4351
|
+
"methodSelector": method_selector,
|
|
4352
|
+
"creditCost": credit_cost,
|
|
4353
|
+
})
|
|
4354
|
+
|
|
4355
|
+
async def violations(self, limit: int = 50, offset: int = 0) -> dict[str, Any]:
|
|
4356
|
+
"""Get violation history."""
|
|
4357
|
+
return await self._http.request("GET", f"/v1/policies/violations?limit={limit}&offset={offset}")
|
|
4358
|
+
|
|
4359
|
+
async def approve_request(self, request_id: str) -> dict[str, Any]:
|
|
4360
|
+
"""Approve a threshold approval request."""
|
|
4361
|
+
return await self._http.request("POST", f"/v1/policies/approvals/{request_id}/approve")
|
|
4362
|
+
|
|
4363
|
+
|
|
4364
|
+
# ============================================================
|
|
4365
|
+
# Delegation Manager
|
|
4366
|
+
# ============================================================
|
|
4367
|
+
|
|
4368
|
+
|
|
4369
|
+
class DelegationManager:
|
|
4370
|
+
"""Manage delegated agent access — create, revoke, and relay via delegation."""
|
|
4371
|
+
|
|
4372
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
4373
|
+
self._http = http
|
|
4374
|
+
|
|
4375
|
+
async def create(
|
|
4376
|
+
self,
|
|
4377
|
+
delegate_id: str,
|
|
4378
|
+
allowed_actions: list[str] | None = None,
|
|
4379
|
+
spending_limit: float | None = None,
|
|
4380
|
+
label: str | None = None,
|
|
4381
|
+
expires_at: str | None = None,
|
|
4382
|
+
) -> dict[str, Any]:
|
|
4383
|
+
"""Create a delegation (authenticated agent = delegator)."""
|
|
4384
|
+
payload: dict[str, Any] = {"delegateId": delegate_id}
|
|
4385
|
+
if allowed_actions:
|
|
4386
|
+
payload["allowedActions"] = allowed_actions
|
|
4387
|
+
if spending_limit is not None:
|
|
4388
|
+
payload["spendingLimit"] = spending_limit
|
|
4389
|
+
if label:
|
|
4390
|
+
payload["label"] = label
|
|
4391
|
+
if expires_at:
|
|
4392
|
+
payload["expiresAt"] = expires_at
|
|
4393
|
+
return await self._http.request("POST", "/v1/delegations", json=payload)
|
|
4394
|
+
|
|
4395
|
+
async def list(self, role: str | None = None, active_only: bool = True) -> dict[str, Any]:
|
|
4396
|
+
"""List delegations."""
|
|
4397
|
+
params: list[str] = []
|
|
4398
|
+
if role:
|
|
4399
|
+
params.append(f"role={role}")
|
|
4400
|
+
if not active_only:
|
|
4401
|
+
params.append("activeOnly=false")
|
|
4402
|
+
qs = ("?" + "&".join(params)) if params else ""
|
|
4403
|
+
return await self._http.request("GET", f"/v1/delegations{qs}")
|
|
4404
|
+
|
|
4405
|
+
async def get(self, delegation_id: str) -> dict[str, Any]:
|
|
4406
|
+
"""Get delegation details."""
|
|
4407
|
+
return await self._http.request("GET", f"/v1/delegations/{delegation_id}")
|
|
4408
|
+
|
|
4409
|
+
async def revoke(self, delegation_id: str) -> dict[str, Any]:
|
|
4410
|
+
"""Revoke a delegation."""
|
|
4411
|
+
return await self._http.request("DELETE", f"/v1/delegations/{delegation_id}")
|
|
4412
|
+
|
|
4413
|
+
async def relay(
|
|
4414
|
+
self,
|
|
4415
|
+
delegation_id: str,
|
|
4416
|
+
target_contract: str,
|
|
4417
|
+
method_selector: str,
|
|
4418
|
+
credit_cost: float = 0,
|
|
4419
|
+
) -> dict[str, Any]:
|
|
4420
|
+
"""Execute a delegated relay."""
|
|
4421
|
+
return await self._http.request("POST", "/v1/relay/delegated", json={
|
|
4422
|
+
"delegationId": delegation_id,
|
|
4423
|
+
"targetContract": target_contract,
|
|
4424
|
+
"methodSelector": method_selector,
|
|
4425
|
+
"creditCost": credit_cost,
|
|
4426
|
+
})
|
|
4427
|
+
|
|
4428
|
+
|
|
4429
|
+
# ============================================================
|
|
4430
|
+
# Treasury Ops Manager
|
|
4431
|
+
# ============================================================
|
|
4432
|
+
|
|
4433
|
+
|
|
4434
|
+
class TreasuryOpsManager:
|
|
4435
|
+
"""Scheduled treasury operations and budget earmarks."""
|
|
4436
|
+
|
|
4437
|
+
def __init__(self, http: _HttpClient) -> None:
|
|
4438
|
+
self._http = http
|
|
4439
|
+
|
|
4440
|
+
async def create_op(
|
|
4441
|
+
self,
|
|
4442
|
+
op_type: str,
|
|
4443
|
+
schedule: dict[str, Any],
|
|
4444
|
+
rules: dict[str, Any],
|
|
4445
|
+
guild_id: str | None = None,
|
|
4446
|
+
) -> dict[str, Any]:
|
|
4447
|
+
"""Create a scheduled treasury operation."""
|
|
4448
|
+
payload: dict[str, Any] = {"opType": op_type, "schedule": schedule, "rules": rules}
|
|
4449
|
+
if guild_id:
|
|
4450
|
+
payload["guildId"] = guild_id
|
|
4451
|
+
return await self._http.request("POST", "/v1/treasury-ops", json=payload)
|
|
4452
|
+
|
|
4453
|
+
async def list_ops(self, guild_id: str | None = None) -> dict[str, Any]:
|
|
4454
|
+
"""List scheduled operations."""
|
|
4455
|
+
qs = f"?guildId={guild_id}" if guild_id else ""
|
|
4456
|
+
return await self._http.request("GET", f"/v1/treasury-ops{qs}")
|
|
4457
|
+
|
|
4458
|
+
async def update_op(self, op_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
4459
|
+
"""Update a scheduled operation."""
|
|
4460
|
+
return await self._http.request("PATCH", f"/v1/treasury-ops/{op_id}", json=kwargs)
|
|
4461
|
+
|
|
4462
|
+
async def delete_op(self, op_id: str) -> dict[str, Any]:
|
|
4463
|
+
"""Delete a scheduled operation."""
|
|
4464
|
+
return await self._http.request("DELETE", f"/v1/treasury-ops/{op_id}")
|
|
4465
|
+
|
|
4466
|
+
async def run_op(self, op_id: str) -> dict[str, Any]:
|
|
4467
|
+
"""Manually trigger an operation."""
|
|
4468
|
+
return await self._http.request("POST", f"/v1/treasury-ops/{op_id}/run")
|
|
4469
|
+
|
|
4470
|
+
async def op_history(self, op_id: str, limit: int = 50) -> dict[str, Any]:
|
|
4471
|
+
"""Get execution history for an operation."""
|
|
4472
|
+
return await self._http.request("GET", f"/v1/treasury-ops/{op_id}/history?limit={limit}")
|
|
4473
|
+
|
|
4474
|
+
async def create_earmark(
|
|
4475
|
+
self,
|
|
4476
|
+
label: str,
|
|
4477
|
+
allocated: float,
|
|
4478
|
+
description: str | None = None,
|
|
4479
|
+
) -> dict[str, Any]:
|
|
4480
|
+
"""Create a budget earmark."""
|
|
4481
|
+
payload: dict[str, Any] = {"label": label, "allocated": allocated}
|
|
4482
|
+
if description:
|
|
4483
|
+
payload["description"] = description
|
|
4484
|
+
return await self._http.request("POST", "/v1/budgets", json=payload)
|
|
4485
|
+
|
|
4486
|
+
async def list_earmarks(self) -> dict[str, Any]:
|
|
4487
|
+
"""List budget earmarks."""
|
|
4488
|
+
return await self._http.request("GET", "/v1/budgets")
|
|
4489
|
+
|
|
4490
|
+
async def update_earmark(self, earmark_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
4491
|
+
"""Update a budget earmark."""
|
|
4492
|
+
return await self._http.request("PATCH", f"/v1/budgets/{earmark_id}", json=kwargs)
|
|
4493
|
+
|
|
4494
|
+
async def delete_earmark(self, earmark_id: str) -> dict[str, Any]:
|
|
4495
|
+
"""Delete a budget earmark."""
|
|
4496
|
+
return await self._http.request("DELETE", f"/v1/budgets/{earmark_id}")
|
|
4497
|
+
|
|
4498
|
+
|
|
4301
4499
|
# ============================================================
|
|
4302
4500
|
# Main Runtime Client
|
|
4303
4501
|
# ============================================================
|
|
@@ -4342,7 +4540,7 @@ class NookplotRuntime:
|
|
|
4342
4540
|
self.discovery = _DiscoveryManager(self._http)
|
|
4343
4541
|
self.bounties = _BountyManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
4344
4542
|
self.bundles = _BundleManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
4345
|
-
self.guilds =
|
|
4543
|
+
self.guilds = _GuildManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
4346
4544
|
self.cliques = self.guilds # Backward-compatible alias
|
|
4347
4545
|
self.communities = _CommunityManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
4348
4546
|
self.marketplace = _MarketplaceManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
|
|
@@ -4354,6 +4552,9 @@ class NookplotRuntime:
|
|
|
4354
4552
|
self.specialization = _SpecializationManager(self._http)
|
|
4355
4553
|
self.intents = _IntentManager(self._http)
|
|
4356
4554
|
self.oracle = _OracleManager(self._http)
|
|
4555
|
+
self.policies = PolicyManager(self._http)
|
|
4556
|
+
self.delegations = DelegationManager(self._http)
|
|
4557
|
+
self.treasury_ops = TreasuryOpsManager(self._http)
|
|
4357
4558
|
|
|
4358
4559
|
# State
|
|
4359
4560
|
self._session_id: str | None = None
|
|
@@ -1013,12 +1013,12 @@ class BundleListResult(BaseModel):
|
|
|
1013
1013
|
|
|
1014
1014
|
|
|
1015
1015
|
# ============================================================
|
|
1016
|
-
#
|
|
1016
|
+
# Guilds
|
|
1017
1017
|
# ============================================================
|
|
1018
1018
|
|
|
1019
1019
|
|
|
1020
|
-
class
|
|
1021
|
-
"""A member of a
|
|
1020
|
+
class GuildMember(BaseModel):
|
|
1021
|
+
"""A member of a guild.
|
|
1022
1022
|
|
|
1023
1023
|
Gateway returns enriched member objects: ``{address, status}``
|
|
1024
1024
|
where status is 0=None, 1=Invited, 2=Accepted, 3=Rejected, 4=Left.
|
|
@@ -1031,27 +1031,39 @@ class CliqueMember(BaseModel):
|
|
|
1031
1031
|
model_config = {"populate_by_name": True}
|
|
1032
1032
|
|
|
1033
1033
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1034
|
+
# Deprecated alias
|
|
1035
|
+
CliqueMember = GuildMember
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
class Guild(BaseModel):
|
|
1039
|
+
"""An on-chain guild (small agent group)."""
|
|
1036
1040
|
|
|
1037
1041
|
id: int
|
|
1038
1042
|
name: str
|
|
1039
1043
|
description: str | None = None
|
|
1040
1044
|
proposer: str | None = None
|
|
1041
1045
|
status: str = "proposed"
|
|
1042
|
-
members: list[
|
|
1046
|
+
members: list[GuildMember] = Field(default_factory=list)
|
|
1043
1047
|
created_at: str | None = Field(None, alias="createdAt")
|
|
1044
1048
|
|
|
1045
1049
|
model_config = {"populate_by_name": True}
|
|
1046
1050
|
|
|
1047
1051
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1052
|
+
# Deprecated alias
|
|
1053
|
+
Clique = Guild
|
|
1054
|
+
|
|
1050
1055
|
|
|
1051
|
-
|
|
1056
|
+
class GuildListResult(BaseModel):
|
|
1057
|
+
"""Result from guild list endpoint."""
|
|
1058
|
+
|
|
1059
|
+
guilds: list[Guild] = Field(default_factory=list)
|
|
1052
1060
|
total: int = 0
|
|
1053
1061
|
|
|
1054
1062
|
|
|
1063
|
+
# Deprecated alias
|
|
1064
|
+
CliqueListResult = GuildListResult
|
|
1065
|
+
|
|
1066
|
+
|
|
1055
1067
|
# ============================================================
|
|
1056
1068
|
# Communities
|
|
1057
1069
|
# ============================================================
|