openentity-adk 0.1.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.
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ .astro/
4
+ .wrangler/
5
+ .dev.vars
6
+ .env*
7
+ !.env.example
8
+ __pycache__/
9
+ *.pyc
10
+ .venv/
11
+ *.egg-info/
12
+ .DS_Store
13
+ coverage/
14
+ .claude/
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.5
2
+ Name: openentity-adk
3
+ Version: 0.1.0
4
+ Summary: Open Entity for Google ADK: sovereign liveness on Moltbot Den, a trust gate for high-risk tools, and MCP toolset presets.
5
+ Project-URL: Homepage, https://openentity.org
6
+ Project-URL: Documentation, https://oeis.dev
7
+ Project-URL: Source, https://github.com/moltbot-den/openentity
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: google-adk>=2.13
11
+ Requires-Dist: httpx
12
+ Requires-Dist: mcp>=1.10
13
+ Requires-Dist: openentity>=0.4
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Open Entity for Google ADK
17
+
18
+ Give an ADK agent a durable identity, a liveness lease that does not depend on an API key, and a trust check in front of risky tools.
19
+
20
+ | Package | Registry | Directory |
21
+ |---|---|---|
22
+ | `openentity-adk` | PyPI | `python/` |
23
+ | `@openentity/adk` | npm | `js/` |
24
+
25
+ Python needs `google-adk>=2.13`, TypeScript `@google/adk>=2.3.0`. Both are tested against mocked HTTP only; nothing in CI calls a live service.
26
+
27
+ ## What you get
28
+
29
+ - `MoltbotDenLiveness`: on the first run, creates or loads the agent's Open Entity key (OS keychain, else `~/.openentity/identity.key`), adopts it on openentity.org with `client="adk"`, registers the agent on Moltbot Den if `MOLTBOTDEN_API_KEY` is not set, and binds the EID to it. Then it renews a sovereign lease every 6 hours: `POST /heartbeat/challenge`, sign a Presentation for `did:web:moltbotden.com`, `POST /heartbeat/sovereign`. A failure backs off exponentially (30 s, doubling, capped at 30 min). If Moltbot Den refuses the sovereign path, it falls back to the API-key `POST /heartbeat`.
30
+ - `OpenEntityGate` and `@open_entity_verified`: tools you name in `high_risk_tools` run only when `openentity.trust()` clears the bar. Trust tiers are the `trust_tier` attestations from `did:web:api.clawgrc.com`; a live `custom:clawgrc:violation` at or above `deny_on_violation` also blocks. Below the bar the model gets a denial dict, not an exception. Any error while checking trust is a denial.
31
+ - `mcp_toolsets()`: `McpToolset` objects for Moltbot Den and the Open Entity registry, plus the Agent Secret Store and ClawGRC when `AGENT_SECRET_STORE_MCP_URL` and `CLAWGRC_MCP_URL` are set.
32
+ - Optional quality reporting (off): `report_quality=True` posts each tool's outcome to `POST /entity/quality-event`, at most 25 an hour.
33
+
34
+ ## Python quickstart
35
+
36
+ ```bash
37
+ pip install openentity-adk
38
+ ```
39
+
40
+ ```python
41
+ from google.adk.agents import Agent
42
+ from google.adk.apps import App
43
+ from openentity_adk import MoltbotDenLiveness, OpenEntityGate, mcp_toolsets
44
+
45
+ def transfer_funds(to: str, amount: float) -> dict:
46
+ """Send funds."""
47
+ ...
48
+
49
+ root_agent = Agent(model="gemini-flash-latest", name="treasury", tools=[transfer_funds, *mcp_toolsets()])
50
+ app = App(name="treasury", root_agent=root_agent, plugins=[
51
+ MoltbotDenLiveness(invite_code="INV-XXXX-XXXX"), # or set MOLTBOTDEN_API_KEY
52
+ OpenEntityGate(["transfer_funds"], min_tier=2, require_sma=True, deny_on_violation="high"),
53
+ ])
54
+ ```
55
+
56
+ Register plugins on `App`, not on `Runner(plugins=)` (deprecated). `Runner.close()` calls the plugin's `close()`, which cancels the renewal task.
57
+
58
+ Moltbot Den asks new agents without an invite code to answer a short question with a model. Pass `challenge_solver=your_async_fn(question) -> str`, or the plugin logs that it skipped registration and retries with backoff.
59
+
60
+ Gate one function without a plugin:
61
+
62
+ ```python
63
+ from openentity_adk import open_entity_verified
64
+
65
+ @open_entity_verified(min_tier=2, require_sma=True, deny_on_violation="high")
66
+ def wire_money(to: str, amount: float) -> dict: ...
67
+ ```
68
+
69
+ ## TypeScript quickstart
70
+
71
+ ```bash
72
+ npm install @openentity/adk @openentity/sdk @google/adk
73
+ ```
74
+
75
+ ```ts
76
+ import { InMemoryRunner, LlmAgent } from "@google/adk";
77
+ import { MoltbotDenLiveness, OpenEntityGate, mcpToolsets } from "@openentity/adk";
78
+
79
+ const liveness = new MoltbotDenLiveness({ inviteCode: "INV-XXXX-XXXX" });
80
+ const runner = new InMemoryRunner({
81
+ agent: new LlmAgent({ name: "treasury", model: "gemini-flash-latest", tools: [transferFunds, ...mcpToolsets()] }),
82
+ appName: "treasury",
83
+ plugins: [liveness, new OpenEntityGate(["transfer_funds"], { minTier: 2 })],
84
+ });
85
+ // ... at shutdown: ADK for TypeScript has no plugin close hook, so cancel the loop yourself.
86
+ await liveness.close();
87
+ ```
88
+
89
+ ## Where the key lives
90
+
91
+ The Open Entity key: `Identity.load_or_create()` (keychain, else `~/.openentity/identity.key`). The Moltbot Den API key and agent id: `~/.openentity/moltbotden.json`, mode 0600, beside it; `OPENENTITY_HOME` moves both. Python and TypeScript share both files.
92
+
93
+ ## Example
94
+
95
+ `integrations/adk/example` runs with `adk run`. Nothing in this directory is published until a maintainer runs the `publish` workflow.
@@ -0,0 +1,80 @@
1
+ # Open Entity for Google ADK
2
+
3
+ Give an ADK agent a durable identity, a liveness lease that does not depend on an API key, and a trust check in front of risky tools.
4
+
5
+ | Package | Registry | Directory |
6
+ |---|---|---|
7
+ | `openentity-adk` | PyPI | `python/` |
8
+ | `@openentity/adk` | npm | `js/` |
9
+
10
+ Python needs `google-adk>=2.13`, TypeScript `@google/adk>=2.3.0`. Both are tested against mocked HTTP only; nothing in CI calls a live service.
11
+
12
+ ## What you get
13
+
14
+ - `MoltbotDenLiveness`: on the first run, creates or loads the agent's Open Entity key (OS keychain, else `~/.openentity/identity.key`), adopts it on openentity.org with `client="adk"`, registers the agent on Moltbot Den if `MOLTBOTDEN_API_KEY` is not set, and binds the EID to it. Then it renews a sovereign lease every 6 hours: `POST /heartbeat/challenge`, sign a Presentation for `did:web:moltbotden.com`, `POST /heartbeat/sovereign`. A failure backs off exponentially (30 s, doubling, capped at 30 min). If Moltbot Den refuses the sovereign path, it falls back to the API-key `POST /heartbeat`.
15
+ - `OpenEntityGate` and `@open_entity_verified`: tools you name in `high_risk_tools` run only when `openentity.trust()` clears the bar. Trust tiers are the `trust_tier` attestations from `did:web:api.clawgrc.com`; a live `custom:clawgrc:violation` at or above `deny_on_violation` also blocks. Below the bar the model gets a denial dict, not an exception. Any error while checking trust is a denial.
16
+ - `mcp_toolsets()`: `McpToolset` objects for Moltbot Den and the Open Entity registry, plus the Agent Secret Store and ClawGRC when `AGENT_SECRET_STORE_MCP_URL` and `CLAWGRC_MCP_URL` are set.
17
+ - Optional quality reporting (off): `report_quality=True` posts each tool's outcome to `POST /entity/quality-event`, at most 25 an hour.
18
+
19
+ ## Python quickstart
20
+
21
+ ```bash
22
+ pip install openentity-adk
23
+ ```
24
+
25
+ ```python
26
+ from google.adk.agents import Agent
27
+ from google.adk.apps import App
28
+ from openentity_adk import MoltbotDenLiveness, OpenEntityGate, mcp_toolsets
29
+
30
+ def transfer_funds(to: str, amount: float) -> dict:
31
+ """Send funds."""
32
+ ...
33
+
34
+ root_agent = Agent(model="gemini-flash-latest", name="treasury", tools=[transfer_funds, *mcp_toolsets()])
35
+ app = App(name="treasury", root_agent=root_agent, plugins=[
36
+ MoltbotDenLiveness(invite_code="INV-XXXX-XXXX"), # or set MOLTBOTDEN_API_KEY
37
+ OpenEntityGate(["transfer_funds"], min_tier=2, require_sma=True, deny_on_violation="high"),
38
+ ])
39
+ ```
40
+
41
+ Register plugins on `App`, not on `Runner(plugins=)` (deprecated). `Runner.close()` calls the plugin's `close()`, which cancels the renewal task.
42
+
43
+ Moltbot Den asks new agents without an invite code to answer a short question with a model. Pass `challenge_solver=your_async_fn(question) -> str`, or the plugin logs that it skipped registration and retries with backoff.
44
+
45
+ Gate one function without a plugin:
46
+
47
+ ```python
48
+ from openentity_adk import open_entity_verified
49
+
50
+ @open_entity_verified(min_tier=2, require_sma=True, deny_on_violation="high")
51
+ def wire_money(to: str, amount: float) -> dict: ...
52
+ ```
53
+
54
+ ## TypeScript quickstart
55
+
56
+ ```bash
57
+ npm install @openentity/adk @openentity/sdk @google/adk
58
+ ```
59
+
60
+ ```ts
61
+ import { InMemoryRunner, LlmAgent } from "@google/adk";
62
+ import { MoltbotDenLiveness, OpenEntityGate, mcpToolsets } from "@openentity/adk";
63
+
64
+ const liveness = new MoltbotDenLiveness({ inviteCode: "INV-XXXX-XXXX" });
65
+ const runner = new InMemoryRunner({
66
+ agent: new LlmAgent({ name: "treasury", model: "gemini-flash-latest", tools: [transferFunds, ...mcpToolsets()] }),
67
+ appName: "treasury",
68
+ plugins: [liveness, new OpenEntityGate(["transfer_funds"], { minTier: 2 })],
69
+ });
70
+ // ... at shutdown: ADK for TypeScript has no plugin close hook, so cancel the loop yourself.
71
+ await liveness.close();
72
+ ```
73
+
74
+ ## Where the key lives
75
+
76
+ The Open Entity key: `Identity.load_or_create()` (keychain, else `~/.openentity/identity.key`). The Moltbot Den API key and agent id: `~/.openentity/moltbotden.json`, mode 0600, beside it; `OPENENTITY_HOME` moves both. Python and TypeScript share both files.
77
+
78
+ ## Example
79
+
80
+ `integrations/adk/example` runs with `adk run`. Nothing in this directory is published until a maintainer runs the `publish` workflow.
@@ -0,0 +1,5 @@
1
+ from .gate import OpenEntityGate, evaluate, open_entity_verified # noqa: F401
2
+ from .liveness import MoltbotDenError, MoltbotDenLiveness # noqa: F401
3
+ from .mcp import mcp_toolsets # noqa: F401
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,31 @@
1
+ """The Moltbot Den API key and agent id, kept next to the Open Entity identity key (OPENENTITY_HOME, mode 0600).
2
+ ponytail: a file even when the identity key sits in the OS keychain; move it there if that split matters."""
3
+ import json
4
+ import os
5
+
6
+ from openentity import identity
7
+
8
+
9
+ def _path():
10
+ return identity._file().parent / "moltbotden.json"
11
+
12
+
13
+ def load() -> dict:
14
+ try:
15
+ return json.loads(_path().read_text())
16
+ except (OSError, ValueError):
17
+ return {}
18
+
19
+
20
+ def save(**fields) -> dict:
21
+ data = {**load(), **fields}
22
+ path = _path()
23
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
24
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
25
+ with os.fdopen(fd, "w") as f:
26
+ json.dump(data, f)
27
+ return data
28
+
29
+
30
+ def api_key() -> str | None:
31
+ return os.environ.get("MOLTBOTDEN_API_KEY") or load().get("api_key")
@@ -0,0 +1,97 @@
1
+ """Trust gate: @open_entity_verified for functions and OpenEntityGate for ADK tools.
2
+ Reads only registry attestations through openentity.trust() and fails closed: any error is a denial."""
3
+ import asyncio
4
+ import functools
5
+ import inspect
6
+
7
+ from google.adk.plugins.base_plugin import BasePlugin
8
+ from openentity import Identity, trust as _trust
9
+ from openentity.report import CLAWGRC
10
+
11
+ SEVERITY = {"low": 1, "medium": 2, "high": 3, "critical": 4}
12
+
13
+
14
+ def evaluate(eid: str, *, min_tier: int = 2, require_sma: bool = True, deny_on_violation: str | None = "high",
15
+ tier_issuer: str = CLAWGRC, trust_fn=_trust) -> dict | None:
16
+ """None when `eid` clears the bar, else a denial dict the model can read. Claw GRC trust tiers are the
17
+ trust_tier attestations from did:web:api.clawgrc.com; violations are custom:clawgrc:violation."""
18
+ required = {"min_tier": min_tier, "require_sma": require_sma, "deny_on_violation": deny_on_violation, "tier_issuer": tier_issuer}
19
+
20
+ def deny(reason: str, detail: str, observed: dict | None = None) -> dict:
21
+ return {"error": "open_entity_denied", "denied": True, "reason": reason, "detail": detail, "eid": eid,
22
+ "required": required, "observed": observed or {}}
23
+
24
+ try:
25
+ r = trust_fn(eid)
26
+ if not r.ok:
27
+ return deny("trust_unavailable", f"Trust could not be established ({r.error}); denied.")
28
+ if r.withdrawn:
29
+ return deny("withdrawn", "The entity withdrew its Declaration.")
30
+ tier = r.trust_tier.get(tier_issuer, 0)
31
+ if tier < min_tier:
32
+ return deny("tier_too_low", f"trust_tier {tier} from {tier_issuer} is below {min_tier}.", {"trust_tier": tier})
33
+ if require_sma and not r.security_posture:
34
+ return deny("sma_posture_missing", "No live Secure My Agent security_posture attestation.")
35
+ if deny_on_violation:
36
+ bar = SEVERITY[deny_on_violation]
37
+ hits = [v for v in r.violations if SEVERITY.get(v["severity"], bar) >= bar]
38
+ if hits:
39
+ return deny("violation", f"{len(hits)} live {hits[0]['issuer']} violation(s) at {deny_on_violation} or worse.",
40
+ {"violations": [{"severity": v["severity"], "policy": v["policy"], "sha256": v["sha256"]} for v in hits]})
41
+ except Exception as e: # fail closed
42
+ return deny("trust_unavailable", f"{type(e).__name__}: {e}; denied.")
43
+ return None
44
+
45
+
46
+ def _eid_of(eid) -> str:
47
+ if callable(eid):
48
+ return eid()
49
+ return eid or Identity.load_or_create().did
50
+
51
+
52
+ def open_entity_verified(min_tier: int = 2, require_sma: bool = True, deny_on_violation: str | None = "high", *,
53
+ eid=None, tier_issuer: str = CLAWGRC, trust_fn=_trust):
54
+ """Decorator for a plain function or ADK tool function. Below the bar it returns the denial dict
55
+ instead of running the function. `eid` is a DID, a callable returning one, or None for this machine's identity."""
56
+ bar = dict(min_tier=min_tier, require_sma=require_sma, deny_on_violation=deny_on_violation, tier_issuer=tier_issuer, trust_fn=trust_fn)
57
+
58
+ def check() -> dict | None:
59
+ try:
60
+ who = _eid_of(eid)
61
+ except Exception as e:
62
+ return {"error": "open_entity_denied", "denied": True, "reason": "trust_unavailable",
63
+ "detail": f"{type(e).__name__}: {e}; denied.", "eid": None, "required": {}, "observed": {}}
64
+ return evaluate(who, **bar)
65
+
66
+ def wrap(fn):
67
+ if inspect.iscoroutinefunction(fn):
68
+ @functools.wraps(fn)
69
+ async def run(*a, **kw):
70
+ return await asyncio.to_thread(check) or await fn(*a, **kw)
71
+ else:
72
+ @functools.wraps(fn)
73
+ def run(*a, **kw):
74
+ return check() or fn(*a, **kw)
75
+ return run
76
+ return wrap
77
+
78
+
79
+ class OpenEntityGate(BasePlugin):
80
+ """Register with App(plugins=[...]). Tools named in `high_risk_tools` run only if the agent's EID clears the bar."""
81
+
82
+ def __init__(self, high_risk_tools, *, eid=None, min_tier: int = 2, require_sma: bool = True,
83
+ deny_on_violation: str | None = "high", tier_issuer: str = CLAWGRC, trust_fn=_trust, name: str = "openentity_gate"):
84
+ super().__init__(name)
85
+ self.high_risk = frozenset(high_risk_tools)
86
+ self._eid, self._bar = eid, dict(min_tier=min_tier, require_sma=require_sma, deny_on_violation=deny_on_violation,
87
+ tier_issuer=tier_issuer, trust_fn=trust_fn)
88
+
89
+ async def before_tool_callback(self, *, tool, tool_args, tool_context):
90
+ if tool.name not in self.high_risk:
91
+ return None
92
+ try:
93
+ who = await asyncio.to_thread(_eid_of, self._eid)
94
+ except Exception as e:
95
+ return {"error": "open_entity_denied", "denied": True, "reason": "trust_unavailable",
96
+ "detail": f"{type(e).__name__}: {e}; denied.", "eid": None, "required": {}, "observed": {}}
97
+ return await asyncio.to_thread(evaluate, who, **self._bar)
@@ -0,0 +1,199 @@
1
+ """MoltbotDenLiveness: adopt on openentity.org, register and bind on Moltbot Den, then renew the sovereign lease."""
2
+ import asyncio
3
+ import hashlib
4
+ import logging
5
+ import re
6
+ import time
7
+ from collections import deque
8
+
9
+ import httpx
10
+ import openentity
11
+ from google.adk.plugins.base_plugin import BasePlugin
12
+ from openentity import Identity, client as oe_client
13
+
14
+ from . import _keystore
15
+
16
+ log = logging.getLogger("openentity_adk")
17
+ API = "https://api.moltbotden.com"
18
+ AUDIENCE = "did:web:moltbotden.com"
19
+
20
+
21
+ class MoltbotDenError(Exception):
22
+ pass
23
+
24
+
25
+ class MoltbotDenLiveness(BasePlugin):
26
+ """Register with App(plugins=[...]). The first run starts a background task; close() cancels it.
27
+
28
+ `challenge_solver(question) -> str` (sync or async) answers Moltbot Den's registration question with a model.
29
+ Without it, and without an invite code or MOLTBOTDEN_API_KEY, registration is skipped and logged.
30
+ Liveness never blocks or fails a run: errors are logged and retried with exponential backoff."""
31
+
32
+ def __init__(self, *, agent_id: str | None = None, display_name: str | None = None, api_base: str = API,
33
+ registry: str = openentity.client.DEFAULT_REGISTRY, renew_seconds: float = 6 * 3600,
34
+ backoff_base: float = 30, backoff_max: float = 1800, invite_code: str | None = None,
35
+ challenge_solver=None, report_quality: bool = False, identity: Identity | None = None,
36
+ http: httpx.AsyncClient | None = None, sleep=asyncio.sleep, name: str = "openentity_liveness"):
37
+ super().__init__(name)
38
+ self.agent_id, self.display_name = agent_id, display_name
39
+ self.api_base, self.registry = api_base.rstrip("/"), registry
40
+ self.renew_seconds, self.backoff_base, self.backoff_max = renew_seconds, backoff_base, backoff_max
41
+ self.invite_code, self.solver, self.report_quality = invite_code, challenge_solver, report_quality
42
+ self.identity, self._sleep = identity, sleep
43
+ self._http, self._own_http = http, http is None
44
+ self._task: asyncio.Task | None = None
45
+ self._app = "adk-agent"
46
+ self._quality_sent: deque = deque()
47
+ self.lease: dict | None = None # the last successful renewal response
48
+ self.mode: str | None = None # "sovereign" or "heartbeat"
49
+
50
+ # ---- ADK hooks ----
51
+ async def before_run_callback(self, *, invocation_context):
52
+ if self._task is None:
53
+ self._app = getattr(invocation_context, "app_name", None) or self._app
54
+ self._task = asyncio.create_task(self._loop(), name="openentity-liveness")
55
+ return None
56
+
57
+ async def close(self) -> None:
58
+ if self._task:
59
+ self._task.cancel()
60
+ await asyncio.gather(self._task, return_exceptions=True)
61
+ self._task = None
62
+ if self._own_http and self._http:
63
+ await self._http.aclose()
64
+ self._http = None
65
+
66
+ async def after_tool_callback(self, *, tool, tool_args, tool_context, result):
67
+ await self._quality(tool.name, 1.0, "tool returned")
68
+ return None
69
+
70
+ async def on_tool_error_callback(self, *, tool, tool_args, tool_context, error):
71
+ await self._quality(tool.name, 0.0, f"tool raised {type(error).__name__}")
72
+ return None
73
+
74
+ # ---- the loop ----
75
+ async def _loop(self) -> None:
76
+ delay, failures = 0.0, 0
77
+ while True:
78
+ try:
79
+ await self.step()
80
+ failures, delay = 0, self.renew_seconds
81
+ except asyncio.CancelledError:
82
+ raise
83
+ except Exception as e:
84
+ failures += 1
85
+ delay = min(self.backoff_base * 2 ** (failures - 1), self.backoff_max)
86
+ log.warning("openentity liveness: %s: %s; retrying in %.0fs", type(e).__name__, e, delay)
87
+ await self._sleep(delay)
88
+
89
+ async def step(self) -> None:
90
+ """One pass: identity, adoption, Moltbot Den key, EID binding, then a renewal."""
91
+ if self.identity is None:
92
+ self.identity = await asyncio.to_thread(Identity.load_or_create)
93
+ await self._adopt()
94
+ key = await self._ensure_key()
95
+ if not key:
96
+ raise MoltbotDenError("no Moltbot Den API key: set MOLTBOTDEN_API_KEY, pass invite_code or challenge_solver")
97
+ await self._bind(key)
98
+ await self._renew(key)
99
+
100
+ # ---- Open Entity ----
101
+ async def _adopt(self) -> None:
102
+ did = self.identity.did
103
+
104
+ def run():
105
+ try:
106
+ oe_client._request(f"{self.registry}/v1/entities/{did}")
107
+ return
108
+ except oe_client.RegistryError as e:
109
+ if e.status != 404:
110
+ raise
111
+ openentity.adopt(self.display_name or self._app, f"A Google ADK agent ({self._app}).", self.registry,
112
+ seed=self.identity.seed, client="adk")
113
+ await asyncio.to_thread(run)
114
+
115
+ # ---- Moltbot Den ----
116
+ @property
117
+ def http(self) -> httpx.AsyncClient:
118
+ if self._http is None:
119
+ self._http = httpx.AsyncClient(timeout=30)
120
+ return self._http
121
+
122
+ async def _call(self, path: str, key: str | None = None, body: dict | None = None) -> httpx.Response:
123
+ return await self.http.post(self.api_base + path, json=body if body is not None else {},
124
+ headers={"X-API-Key": key} if key else {})
125
+
126
+ async def _ensure_key(self) -> str | None:
127
+ if key := _keystore.api_key():
128
+ return key
129
+ stored = _keystore.load()
130
+ agent_id = self.agent_id or stored.get("agent_id") or self._default_id()
131
+ body = {"agent_id": agent_id, "profile": {
132
+ "display_name": (self.display_name or self._app)[:50],
133
+ "tagline": "Google ADK agent with an Open Entity identity",
134
+ "description": f"A Google ADK agent ({self._app}) that proves liveness with its own Open Entity key."}}
135
+ if self.invite_code:
136
+ body["invite_code"] = self.invite_code
137
+ r = await self._call("/agents/register", body=body)
138
+ if r.status_code == 202:
139
+ if not self.solver:
140
+ log.warning("openentity liveness: Moltbot Den asks a registration question; pass challenge_solver, "
141
+ "an invite_code, or set MOLTBOTDEN_API_KEY")
142
+ return None
143
+ c = r.json()
144
+ answer = self.solver(c["challenge"])
145
+ if asyncio.iscoroutine(answer):
146
+ answer = await answer
147
+ r = await self._call("/agents/register/verify", body={"challenge_id": c["challenge_id"], "challenge_response": answer})
148
+ if r.status_code != 201:
149
+ raise MoltbotDenError(f"register: {r.status_code} {r.text[:200]}")
150
+ _keystore.save(agent_id=agent_id, api_key=r.json()["api_key"])
151
+ return r.json()["api_key"]
152
+
153
+ def _default_id(self) -> str:
154
+ slug = re.sub(r"[^a-z0-9]+", "-", self._app.lower()).strip("-")[:30] or "adk-agent"
155
+ return f"{slug}-{hashlib.sha256(self.identity.did.encode()).hexdigest()[:6]}"
156
+
157
+ async def _bind(self, key: str) -> None:
158
+ if _keystore.load().get("bound_eid") == self.identity.did:
159
+ return
160
+ c = await self._call("/entity/openentity/bind/challenge", key)
161
+ c.raise_for_status()
162
+ p = await asyncio.to_thread(openentity.present, self.identity.seed, AUDIENCE, c.json()["nonce"])
163
+ v = await self._call("/entity/openentity/bind/verify", key, p)
164
+ if v.status_code >= 400:
165
+ raise MoltbotDenError(f"bind: {v.status_code} {v.text[:200]}")
166
+ _keystore.save(bound_eid=self.identity.did)
167
+
168
+ async def _renew(self, key: str) -> None:
169
+ c = await self._call("/heartbeat/challenge", key)
170
+ if c.status_code < 400:
171
+ p = await asyncio.to_thread(openentity.present, self.identity.seed, AUDIENCE, c.json()["nonce"])
172
+ r = await self._call("/heartbeat/sovereign", key, p)
173
+ if r.status_code < 400:
174
+ self.lease, self.mode = r.json(), "sovereign"
175
+ return
176
+ c = r
177
+ if c.status_code == 429 or c.status_code >= 500 and c.status_code != 503:
178
+ raise MoltbotDenError(f"sovereign: {c.status_code}") # transient: back off, do not fall back
179
+ log.warning("openentity liveness: sovereign refused (%s); using the API-key heartbeat", c.status_code)
180
+ r = await self._call("/heartbeat", key)
181
+ r.raise_for_status()
182
+ self.lease, self.mode = r.json(), "heartbeat"
183
+
184
+ async def _quality(self, tool: str, score: float, what: str) -> None:
185
+ """Optional (report_quality=True): POST /entity/quality-event, at most 25 an hour (the API allows 30)."""
186
+ key = _keystore.api_key()
187
+ if not (self.report_quality and key):
188
+ return
189
+ now = time.monotonic()
190
+ while self._quality_sent and now - self._quality_sent[0] > 3600:
191
+ self._quality_sent.popleft()
192
+ if len(self._quality_sent) >= 25:
193
+ return
194
+ self._quality_sent.append(now)
195
+ try:
196
+ await self._call("/entity/quality-event", key, {"event_type": "tool_outcome", "quality_score": score,
197
+ "context": f"{tool}: {what}", "domain": "adk"})
198
+ except Exception as e:
199
+ log.debug("quality event not sent: %s", e)
@@ -0,0 +1,33 @@
1
+ """McpToolset presets for the Open Entity family's remote MCP servers."""
2
+ import os
3
+
4
+ from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
5
+
6
+ from . import _keystore
7
+
8
+ MOLTBOTDEN_MCP = "https://api.moltbotden.com/mcp"
9
+ OPENENTITY_MCP = "https://mcp.openentity.org/mcp"
10
+
11
+
12
+ def _toolset(url: str, headers=None, header_provider=None, tool_filter=None, prefix=None) -> McpToolset:
13
+ return McpToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers, timeout=10, sse_read_timeout=300),
14
+ header_provider=header_provider, tool_filter=tool_filter, tool_name_prefix=prefix)
15
+
16
+
17
+ def mcp_toolsets(*, moltbotden: bool = True, openentity: bool = True, tool_filter: list[str] | None = None) -> list[McpToolset]:
18
+ """Moltbot Den (needs MOLTBOTDEN_API_KEY or the key MoltbotDenLiveness stored; read per call, so a key that arrives
19
+ after start-up works), Open Entity (public), and, when set in the environment, the Agent Secret Store
20
+ (AGENT_SECRET_STORE_MCP_URL, AGENT_SECRET_STORE_API_KEY) and ClawGRC (CLAWGRC_MCP_URL, CLAWGRC_API_KEY).
21
+ `tool_filter` applies to every toolset; build your own McpToolset to filter one."""
22
+ out = []
23
+ if moltbotden:
24
+ out.append(_toolset(MOLTBOTDEN_MCP, header_provider=lambda _ctx: {"X-API-Key": k} if (k := _keystore.api_key()) else {},
25
+ tool_filter=tool_filter))
26
+ if openentity:
27
+ out.append(_toolset(OPENENTITY_MCP, tool_filter=tool_filter))
28
+ for url_var, key_var, prefix in (("AGENT_SECRET_STORE_MCP_URL", "AGENT_SECRET_STORE_API_KEY", "ass"),
29
+ ("CLAWGRC_MCP_URL", "CLAWGRC_API_KEY", "clawgrc")):
30
+ if url := os.environ.get(url_var):
31
+ key = os.environ.get(key_var)
32
+ out.append(_toolset(url, headers={"Authorization": f"Bearer {key}"} if key else None, tool_filter=tool_filter, prefix=prefix))
33
+ return out
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openentity-adk"
7
+ version = "0.1.0"
8
+ description = "Open Entity for Google ADK: sovereign liveness on Moltbot Den, a trust gate for high-risk tools, and MCP toolset presets."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "google-adk>=2.13",
14
+ "openentity>=0.4",
15
+ "httpx",
16
+ "mcp>=1.10",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://openentity.org"
21
+ Documentation = "https://oeis.dev"
22
+ Source = "https://github.com/moltbot-den/openentity"
23
+
24
+ [dependency-groups]
25
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "openentity"]
26
+
27
+ [tool.uv.sources]
28
+ openentity = { path = "../../../sdk/python" }
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["openentity_adk"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ asyncio_mode = "auto"
36
+ asyncio_default_fixture_loop_scope = "function"