nookplot-runtime 0.5.21__tar.gz → 0.5.23__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nookplot-runtime
3
- Version: 0.5.21
3
+ Version: 0.5.23
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
@@ -283,6 +283,10 @@ class AutonomousAgent:
283
283
  return f"revision_requested:{data.get('agreementId', '')}"
284
284
  if signal_type == "review_received":
285
285
  return f"review_received:{data.get('agreementId', '')}"
286
+ # Webhook signals
287
+ if signal_type in ("webhook_received", "webhook.received"):
288
+ source = data.get("source", "")
289
+ return f"webhook:{source}:{int(time.time())}"
286
290
  return f"{signal_type}:{addr}:{data.get('channelId', '')}:{data.get('postCid', '')}"
287
291
 
288
292
  async def _handle_signal(self, data: dict[str, Any]) -> None:
@@ -436,6 +440,9 @@ class AutonomousAgent:
436
440
  self._broadcast("action_skipped", f"Received {data.get('rating', '?')}-star review on Agreement #{data.get('agreementId', '?')}", {
437
441
  "signalType": signal_type, "agreementId": data.get("agreementId"), "rating": data.get("rating"),
438
442
  })
443
+ # ── Webhook signals ──
444
+ elif signal_type in ("webhook_received", "webhook.received"):
445
+ await self._handle_webhook_received(data)
439
446
  else:
440
447
  self._broadcast("action_skipped", f"⏭ Unhandled signal type: {signal_type}", {
441
448
  "signalType": signal_type,
@@ -1111,6 +1118,46 @@ class AutonomousAgent:
1111
1118
  "action": "revision_requested", "agreementId": agreement_id, "error": str(exc),
1112
1119
  })
1113
1120
 
1121
+ # ── Webhook signal handler ──
1122
+
1123
+ async def _handle_webhook_received(self, data: dict[str, Any]) -> None:
1124
+ """External webhook pushed an event — decide how to react."""
1125
+ source = data.get("source", "unknown")
1126
+ webhook_payload = data.get("payload", data.get("body", {}))
1127
+ webhook_headers = data.get("headers", {})
1128
+
1129
+ try:
1130
+ payload_preview = str(webhook_payload)[:500] if webhook_payload else "(empty)"
1131
+
1132
+ prompt = (
1133
+ f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
1134
+ "You received a webhook event from an external service.\n"
1135
+ f"Source: {source}\n"
1136
+ f"Payload:\n{wrap_untrusted(payload_preview, 'webhook payload')}\n\n"
1137
+ "Decide what to do with this event. You can:\n"
1138
+ "- egress_request: Make an outbound HTTP request (params: url, method, headers, body)\n"
1139
+ "- execute_tool: Run a registered tool (params: toolName, args)\n"
1140
+ "- send_message: Notify another agent (params: to, content)\n"
1141
+ "- post: Share an update (params: body, community)\n"
1142
+ "- ignore: Take no action\n\n"
1143
+ "Format:\nACTION: <action_type>\nPARAMS: <json params>"
1144
+ )
1145
+
1146
+ assert self._generate_response is not None
1147
+ response = await self._generate_response(prompt)
1148
+ text = (response or "").strip()
1149
+
1150
+ if text and not text.upper().startswith("IGNORE"):
1151
+ await self._parse_and_execute_action(text)
1152
+ else:
1153
+ self._broadcast("action_skipped", f"⏭ Webhook from {source} — no action taken", {
1154
+ "signalType": "webhook_received", "source": source,
1155
+ })
1156
+ except Exception as exc:
1157
+ self._broadcast("error", f"Webhook handling failed: {exc}", {
1158
+ "action": "webhook_received", "source": source, "error": str(exc),
1159
+ })
1160
+
1114
1161
  # ── Bounty application/submission signal handlers ──
1115
1162
 
1116
1163
  async def _handle_bounty_application_submitted(self, data: dict[str, Any]) -> None:
@@ -1966,7 +2013,7 @@ class AutonomousAgent:
1966
2013
  tx_hash = pub.get("txHash") if isinstance(pub, dict) else getattr(pub, "tx_hash", None)
1967
2014
  result = {"cid": pub.get("cid") if isinstance(pub, dict) else getattr(pub, "cid", None), "txHash": tx_hash}
1968
2015
 
1969
- elif action_type == "create_post":
2016
+ elif action_type in ("create_post", "publish"):
1970
2017
  community = payload.get("community", "general")
1971
2018
  title = payload.get("title") or (suggested_content[:100] if suggested_content else "Untitled")
1972
2019
  body = suggested_content or payload.get("body", "")
@@ -2113,7 +2160,7 @@ class AutonomousAgent:
2113
2160
  }
2114
2161
  )
2115
2162
 
2116
- elif action_type == "gateway_commit":
2163
+ elif action_type in ("gateway_commit", "commit_files"):
2117
2164
  pid = payload.get("projectId")
2118
2165
  files = payload.get("files")
2119
2166
  msg = suggested_content or payload.get("message", "Autonomous commit")
@@ -2353,7 +2400,7 @@ class AutonomousAgent:
2353
2400
  task_result = await self._runtime.projects.update_task(proj_id, tid, **kw)
2354
2401
  result = task_result if isinstance(task_result, dict) else {"updated": True}
2355
2402
 
2356
- elif action_type == "find_matching_agents":
2403
+ elif action_type in ("find_matching_agents", "find_agents"):
2357
2404
  skills = payload.get("skills", [])
2358
2405
  if not skills:
2359
2406
  raise ValueError("find_matching_agents requires skills array")
@@ -2580,7 +2627,7 @@ class AutonomousAgent:
2580
2627
  elif action_type == "create_listing":
2581
2628
  # Alias for list_service
2582
2629
  prep = await self._runtime._http.request("POST", "/v1/prepare/service/list", payload)
2583
- relay = await self._sign_and_relay(prep)
2630
+ relay = await self._runtime.memory._sign_and_relay(prep)
2584
2631
  tx_hash = relay.get("txHash", "")
2585
2632
  result = relay
2586
2633
 
@@ -2589,7 +2636,7 @@ class AutonomousAgent:
2589
2636
  if not gid:
2590
2637
  raise ValueError("approve_guild requires guildId")
2591
2638
  prep = await self._runtime._http.request("POST", f"/v1/prepare/guild/{gid}/approve", {})
2592
- relay = await self._sign_and_relay(prep)
2639
+ relay = await self._runtime.memory._sign_and_relay(prep)
2593
2640
  tx_hash = relay.get("txHash", "")
2594
2641
  result = relay
2595
2642
 
@@ -2598,7 +2645,7 @@ class AutonomousAgent:
2598
2645
  if not gid:
2599
2646
  raise ValueError("reject_guild requires guildId")
2600
2647
  prep = await self._runtime._http.request("POST", f"/v1/prepare/guild/{gid}/reject", {})
2601
- relay = await self._sign_and_relay(prep)
2648
+ relay = await self._runtime.memory._sign_and_relay(prep)
2602
2649
  tx_hash = relay.get("txHash", "")
2603
2650
  result = relay
2604
2651
 
@@ -2607,10 +2654,173 @@ class AutonomousAgent:
2607
2654
  if not gid:
2608
2655
  raise ValueError("leave_guild requires guildId")
2609
2656
  prep = await self._runtime._http.request("POST", f"/v1/prepare/guild/{gid}/leave", {})
2610
- relay = await self._sign_and_relay(prep)
2657
+ relay = await self._runtime.memory._sign_and_relay(prep)
2611
2658
  tx_hash = relay.get("txHash", "")
2612
2659
  result = relay
2613
2660
 
2661
+ # ── Bounty access grant/deny ───────────────────────
2662
+ elif action_type == "grant":
2663
+ proj_id = payload.get("projectId")
2664
+ bounty_id = payload.get("bountyId")
2665
+ req_id = payload.get("requestId")
2666
+ if not proj_id or not bounty_id or not req_id:
2667
+ raise ValueError("grant requires projectId, bountyId, requestId")
2668
+ result = await self._runtime._http.request(
2669
+ "POST", f"/v1/projects/{proj_id}/bounties/{bounty_id}/grant-access",
2670
+ {"requestId": req_id},
2671
+ )
2672
+
2673
+ elif action_type == "deny":
2674
+ proj_id = payload.get("projectId")
2675
+ bounty_id = payload.get("bountyId")
2676
+ req_id = payload.get("requestId")
2677
+ if not proj_id or not bounty_id or not req_id:
2678
+ raise ValueError("deny requires projectId, bountyId, requestId")
2679
+ result = await self._runtime._http.request(
2680
+ "POST", f"/v1/projects/{proj_id}/bounties/{bounty_id}/deny-access",
2681
+ {"requestId": req_id},
2682
+ )
2683
+
2684
+ # ── Team invitations ──────────────────────────────
2685
+ elif action_type == "accept_invitation":
2686
+ inv_id = payload.get("invitationId")
2687
+ if not inv_id:
2688
+ raise ValueError("accept_invitation requires invitationId")
2689
+ result = await self._runtime._http.request(
2690
+ "POST", f"/v1/teams/invitations/{inv_id}/accept", {},
2691
+ )
2692
+
2693
+ elif action_type == "decline_invitation":
2694
+ inv_id = payload.get("invitationId")
2695
+ if not inv_id:
2696
+ raise ValueError("decline_invitation requires invitationId")
2697
+ result = await self._runtime._http.request(
2698
+ "POST", f"/v1/teams/invitations/{inv_id}/decline", {},
2699
+ )
2700
+
2701
+ # ── Service update ────────────────────────────────
2702
+ elif action_type == "update_service":
2703
+ listing_id = payload.get("listingId")
2704
+ if not listing_id:
2705
+ raise ValueError("update_service requires listingId")
2706
+ prep = await self._runtime._http.request(
2707
+ "POST", "/v1/prepare/service/update", payload,
2708
+ )
2709
+ relay = await self._runtime.memory._sign_and_relay(prep)
2710
+ tx_hash = relay.get("txHash", "")
2711
+ result = relay
2712
+
2713
+ # ── Additional bounty lifecycle ───────────────────
2714
+ elif action_type == "approve_bounty_work":
2715
+ bounty_id = payload.get("bountyId")
2716
+ if not bounty_id:
2717
+ raise ValueError("approve_bounty_work requires bountyId")
2718
+ prep = await self._runtime._http.request(
2719
+ "POST", f"/v1/prepare/bounty/{bounty_id}/approve", {},
2720
+ )
2721
+ relay = await self._runtime.memory._sign_and_relay(prep)
2722
+ tx_hash = relay.get("txHash", "")
2723
+ result = relay
2724
+
2725
+ elif action_type == "dispute_bounty_work":
2726
+ bounty_id = payload.get("bountyId")
2727
+ if not bounty_id:
2728
+ raise ValueError("dispute_bounty_work requires bountyId")
2729
+ prep = await self._runtime._http.request(
2730
+ "POST", f"/v1/prepare/bounty/{bounty_id}/dispute", {},
2731
+ )
2732
+ relay = await self._runtime.memory._sign_and_relay(prep)
2733
+ tx_hash = relay.get("txHash", "")
2734
+ result = relay
2735
+
2736
+ elif action_type == "cancel_bounty":
2737
+ bounty_id = payload.get("bountyId")
2738
+ if not bounty_id:
2739
+ raise ValueError("cancel_bounty requires bountyId")
2740
+ prep = await self._runtime._http.request(
2741
+ "POST", f"/v1/prepare/bounty/{bounty_id}/cancel", {},
2742
+ )
2743
+ relay = await self._runtime.memory._sign_and_relay(prep)
2744
+ tx_hash = relay.get("txHash", "")
2745
+ result = relay
2746
+
2747
+ elif action_type == "unclaim_bounty":
2748
+ bounty_id = payload.get("bountyId")
2749
+ if not bounty_id:
2750
+ raise ValueError("unclaim_bounty requires bountyId")
2751
+ prep = await self._runtime._http.request(
2752
+ "POST", f"/v1/prepare/bounty/{bounty_id}/unclaim", {},
2753
+ )
2754
+ relay = await self._runtime.memory._sign_and_relay(prep)
2755
+ tx_hash = relay.get("txHash", "")
2756
+ result = relay
2757
+
2758
+ # ── Real World Actions ──────────────────────────────
2759
+ elif action_type in ("egress_request", "http_request"):
2760
+ # Outbound HTTP request via the egress proxy (0.15 credits)
2761
+ target_url = payload.get("url", "")
2762
+ http_method = (payload.get("method", "GET")).upper()
2763
+ if not target_url:
2764
+ raise ValueError("egress_request requires url")
2765
+ http_result = await self._runtime.tools.http_request(
2766
+ url=target_url,
2767
+ method=http_method,
2768
+ headers=payload.get("headers"),
2769
+ body=payload.get("body"),
2770
+ timeout=payload.get("timeout"),
2771
+ credential_service=payload.get("credentialService"),
2772
+ )
2773
+ result = http_result if isinstance(http_result, dict) else {"response": http_result}
2774
+
2775
+ elif action_type == "execute_tool":
2776
+ # Execute a registered tool through the action registry
2777
+ tool_name = payload.get("toolName") or payload.get("name", "")
2778
+ tool_args = payload.get("args") or payload.get("input", {})
2779
+ if not tool_name:
2780
+ raise ValueError("execute_tool requires toolName")
2781
+ tool_result = await self._runtime.tools.execute_tool(tool_name, tool_args)
2782
+ result = tool_result if isinstance(tool_result, dict) else {"output": tool_result}
2783
+
2784
+ elif action_type == "connect_mcp_server":
2785
+ # Connect to an external MCP server
2786
+ server_url = payload.get("serverUrl", "")
2787
+ server_name = payload.get("serverName") or payload.get("name", "")
2788
+ if not server_url or not server_name:
2789
+ raise ValueError("connect_mcp_server requires serverUrl and serverName")
2790
+ mcp_result = await self._runtime.tools.connect_mcp_server(server_url, server_name)
2791
+ result = mcp_result if isinstance(mcp_result, dict) else {"connected": True}
2792
+
2793
+ elif action_type == "disconnect_mcp_server":
2794
+ server_id = payload.get("serverId", "")
2795
+ if not server_id:
2796
+ raise ValueError("disconnect_mcp_server requires serverId")
2797
+ await self._runtime.tools.disconnect_mcp_server(server_id)
2798
+ result = {"disconnected": True, "serverId": server_id}
2799
+
2800
+ elif action_type in ("call_mcp_tool", "use_mcp_tool"):
2801
+ # Call a tool from a connected MCP server — goes through action registry
2802
+ mcp_tool_name = payload.get("toolName") or payload.get("name", "")
2803
+ mcp_tool_args = payload.get("args") or payload.get("input", {})
2804
+ if not mcp_tool_name:
2805
+ raise ValueError("call_mcp_tool requires toolName")
2806
+ mcp_tool_result = await self._runtime.tools.execute_tool(mcp_tool_name, mcp_tool_args)
2807
+ result = mcp_tool_result if isinstance(mcp_tool_result, dict) else {"output": mcp_tool_result}
2808
+
2809
+ elif action_type == "register_webhook":
2810
+ # Register a webhook source for inbound events
2811
+ source = payload.get("source") or payload.get("name", "")
2812
+ if not source:
2813
+ raise ValueError("register_webhook requires source")
2814
+ config: dict[str, Any] = {}
2815
+ if payload.get("secret"):
2816
+ config["secret"] = payload["secret"]
2817
+ if payload.get("signatureHeader"):
2818
+ config["signatureHeader"] = payload["signatureHeader"]
2819
+ if payload.get("eventMapping"):
2820
+ config["eventMapping"] = payload["eventMapping"]
2821
+ reg_result = await self._runtime.tools.register_webhook(source, config or None)
2822
+ result = reg_result if isinstance(reg_result, dict) else {"registered": True}
2823
+
2614
2824
  else:
2615
2825
  self._broadcast("action_skipped", f"⏭ Unknown action: {action_type}", {
2616
2826
  "action": action_type, "actionId": action_id,
@@ -1943,6 +1943,84 @@ class _ToolManager:
1943
1943
  data = await self._http.request("GET", "/v1/agents/me/mcp/tools")
1944
1944
  return data.get("data", [])
1945
1945
 
1946
+ # ── Webhook Management ──
1947
+
1948
+ async def register_webhook(
1949
+ self,
1950
+ source: str,
1951
+ config: dict[str, Any] | None = None,
1952
+ ) -> dict[str, Any]:
1953
+ """Register a webhook source so external services can push events."""
1954
+ data = await self._http.request(
1955
+ "POST",
1956
+ "/v1/agents/me/webhooks",
1957
+ {"source": source, "config": config or {}},
1958
+ )
1959
+ return data.get("data", {})
1960
+
1961
+ async def list_webhooks(self) -> list[dict[str, Any]]:
1962
+ """List registered webhook sources."""
1963
+ data = await self._http.request("GET", "/v1/agents/me/webhooks")
1964
+ return data.get("data", [])
1965
+
1966
+ async def remove_webhook(self, source: str) -> None:
1967
+ """Remove a webhook registration."""
1968
+ await self._http.request(
1969
+ "DELETE", f"/v1/agents/me/webhooks/{url_quote(source)}"
1970
+ )
1971
+
1972
+ async def get_webhook_log(self, page: int = 0) -> list[dict[str, Any]]:
1973
+ """Get webhook event log (recent deliveries)."""
1974
+ data = await self._http.request(
1975
+ "GET", f"/v1/agents/me/webhooks/log?page={page}"
1976
+ )
1977
+ return data.get("data", [])
1978
+
1979
+ # ── Egress Allowlist Management ──
1980
+
1981
+ async def get_egress_allowlist(self) -> list[dict[str, Any]]:
1982
+ """Get the egress allowlist (domains this agent is allowed to call)."""
1983
+ data = await self._http.request("GET", "/v1/agents/me/egress")
1984
+ return data.get("allowlist", [])
1985
+
1986
+ async def add_egress_domain(
1987
+ self,
1988
+ domain: str,
1989
+ max_requests_per_hour: int | None = None,
1990
+ ) -> dict[str, Any]:
1991
+ """Add or update a domain on the egress allowlist."""
1992
+ payload: dict[str, Any] = {"domain": domain}
1993
+ if max_requests_per_hour is not None:
1994
+ payload["maxRequestsPerHour"] = max_requests_per_hour
1995
+ return await self._http.request("PUT", "/v1/agents/me/egress", payload)
1996
+
1997
+ async def remove_egress_domain(self, domain: str) -> None:
1998
+ """Remove a domain from the egress allowlist."""
1999
+ await self._http.request(
2000
+ "PUT", "/v1/agents/me/egress", {"domain": domain, "remove": True}
2001
+ )
2002
+
2003
+ # ── Credential Management ──
2004
+
2005
+ async def store_credential(self, service: str, api_key: str) -> None:
2006
+ """Store an encrypted API credential for use with egress requests."""
2007
+ await self._http.request(
2008
+ "POST",
2009
+ "/v1/agents/me/credentials",
2010
+ {"service": service, "apiKey": api_key},
2011
+ )
2012
+
2013
+ async def list_credentials(self) -> list[dict[str, Any]]:
2014
+ """List stored credential services (names only — keys are never exposed)."""
2015
+ data = await self._http.request("GET", "/v1/agents/me/credentials")
2016
+ return data.get("credentials", [])
2017
+
2018
+ async def remove_credential(self, service: str) -> None:
2019
+ """Remove a stored credential."""
2020
+ await self._http.request(
2021
+ "DELETE", f"/v1/agents/me/credentials/{url_quote(service)}"
2022
+ )
2023
+
1946
2024
 
1947
2025
  # ============================================================
1948
2026
  # Proactive Manager
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nookplot-runtime"
7
- version = "0.5.21"
7
+ version = "0.5.23"
8
8
  description = "Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"