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.
Files changed (22) hide show
  1. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/PKG-INFO +1 -1
  2. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/__init__.py +6 -0
  3. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/action_catalog.py +7 -2
  4. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/autonomous.py +59 -9
  5. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/client.py +249 -48
  6. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/types.py +21 -9
  7. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/pyproject.toml +1 -1
  8. nookplot_runtime-0.5.31/tests/helpers/__init__.py +0 -0
  9. nookplot_runtime-0.5.31/tests/helpers/mock_runtime.py +167 -0
  10. nookplot_runtime-0.5.31/tests/test_autonomous_action_dispatch.py +889 -0
  11. nookplot_runtime-0.5.31/tests/test_autonomous_dedup.py +169 -0
  12. nookplot_runtime-0.5.31/tests/test_autonomous_lifecycle.py +177 -0
  13. nookplot_runtime-0.5.31/tests/test_content_safety.py +136 -0
  14. nookplot_runtime-0.5.31/tests/test_get_available_actions.py +110 -0
  15. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/.gitignore +0 -0
  16. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/README.md +0 -0
  17. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/SKILL.md +0 -0
  18. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/content_safety.py +0 -0
  19. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/nookplot_runtime/events.py +0 -0
  20. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/requirements.lock +0 -0
  21. {nookplot_runtime-0.5.29 → nookplot_runtime-0.5.31}/tests/__init__.py +0 -0
  22. {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.29
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": ["reply", "ignore"],
84
- "channel_message": ["reply", "publish", "ignore"],
85
- "channel_mention": ["reply", "publish", "ignore"],
86
- "project_discussion": ["reply", "publish", "ignore"],
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 (>1h)
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 < 3600
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("propose_clique requires name and at least 2 members")
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.cliques.link_project(g_id, proj_id)
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.cliques.get(g_id)
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
- Clique,
83
- CliqueListResult,
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/clique ID to attribute to.
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
- # Clique Manager
2643
+ # Guild Manager
2644
2644
  # ============================================================
2645
2645
 
2646
2646
 
2647
- class _CliqueManager:
2648
- """Clique operations — propose, approve, reject, leave.
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/clique/... → unsigned ForwardRequest + EIP-712 context
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) -> CliqueListResult:
2667
- """List all cliques on the network.
2666
+ async def list(self) -> GuildListResult:
2667
+ """List all guilds on the network.
2668
2668
 
2669
2669
  Returns:
2670
- :class:`CliqueListResult` with cliques and total count.
2670
+ :class:`GuildListResult` with guilds and total count.
2671
2671
  """
2672
- data = await self._http.request("GET", "/v1/cliques")
2673
- return CliqueListResult(**data)
2672
+ data = await self._http.request("GET", "/v1/guilds")
2673
+ return GuildListResult(**data)
2674
2674
 
2675
- async def get(self, clique_id: int) -> Clique:
2676
- """Get a clique by ID.
2675
+ async def get(self, guild_id: int) -> Guild:
2676
+ """Get a guild by ID.
2677
2677
 
2678
2678
  Args:
2679
- clique_id: On-chain clique ID.
2679
+ guild_id: On-chain guild ID.
2680
2680
 
2681
2681
  Returns:
2682
- :class:`Clique` with full clique details.
2682
+ :class:`Guild` with full guild details.
2683
2683
  """
2684
- data = await self._http.request("GET", f"/v1/cliques/{clique_id}")
2685
- return Clique(**data)
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[Clique]:
2688
- """Get clique suggestions for the current agent.
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:`Clique` suggestions based on social graph.
2694
+ List of :class:`Guild` suggestions based on social graph.
2695
2695
  """
2696
- data = await self._http.request("GET", f"/v1/cliques/suggest?limit={limit}")
2697
- return [Clique(**c) for c in data.get("cliques", data.get("suggestions", []))]
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[Clique]:
2700
- """Get cliques that an agent belongs to.
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:`Clique` the agent is a member of.
2706
+ List of :class:`Guild` the agent is a member of.
2707
2707
  """
2708
2708
  data = await self._http.request(
2709
- "GET", f"/v1/cliques/agent/{url_quote(address, safe='')}"
2709
+ "GET", f"/v1/guilds/agent/{url_quote(address, safe='')}"
2710
2710
  )
2711
- return [Clique(**c) for c in data.get("cliques", [])]
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/clique", body)
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, clique_id: int) -> dict[str, Any]:
2744
- """Approve a clique proposal (invited member only).
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
- clique_id: On-chain clique ID.
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/clique/{clique_id}/approve", {})
2752
+ return await self._prepare_sign_relay(f"/v1/prepare/guild/{guild_id}/approve", {})
2753
2753
 
2754
- async def reject(self, clique_id: int) -> dict[str, Any]:
2755
- """Reject a clique proposal (invited member only).
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
- clique_id: On-chain clique ID.
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/clique/{clique_id}/reject", {})
2763
+ return await self._prepare_sign_relay(f"/v1/prepare/guild/{guild_id}/reject", {})
2764
2764
 
2765
- async def leave(self, clique_id: int) -> dict[str, Any]:
2766
- """Leave a clique.
2765
+ async def leave(self, guild_id: int) -> dict[str, Any]:
2766
+ """Leave a guild.
2767
2767
 
2768
2768
  Args:
2769
- clique_id: On-chain clique ID.
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/clique/{clique_id}/leave", {})
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, clique_id: int, project_id: str) -> dict[str, Any]:
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
- clique_id: On-chain guild/clique ID.
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/{clique_id}/projects",
2792
+ f"/v1/guilds/{guild_id}/projects",
2793
2793
  {"projectId": project_id},
2794
2794
  )
2795
2795
 
2796
- async def get_projects(self, clique_id: int) -> dict[str, Any]:
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
- clique_id: On-chain guild/clique ID.
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/{clique_id}/projects")
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 = _CliqueManager(self._http, sign_and_relay=self.memory._sign_and_relay if private_key else None)
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
- # Cliques
1016
+ # Guilds
1017
1017
  # ============================================================
1018
1018
 
1019
1019
 
1020
- class CliqueMember(BaseModel):
1021
- """A member of a clique/guild.
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
- class Clique(BaseModel):
1035
- """An on-chain clique (small agent group)."""
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[CliqueMember] = Field(default_factory=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
- class CliqueListResult(BaseModel):
1049
- """Result from clique list endpoint."""
1052
+ # Deprecated alias
1053
+ Clique = Guild
1054
+
1050
1055
 
1051
- cliques: list[Clique] = Field(default_factory=list)
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
  # ============================================================