@openentity/adk 0.1.0 → 0.2.1

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.
package/LICENSE CHANGED
@@ -19,6 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
23
- The specification (`spec/`) and the skill (`skill/`) are dedicated to the
24
- public domain under CC0-1.0; see the LICENSE file in each directory.
package/README.md CHANGED
@@ -1,22 +1,21 @@
1
- # Open Entity for Google ADK
1
+ # Open Entity for ADK
2
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.
3
+ Check another agent's trust record before a risky tool runs. Before your agent pays, hires or shares data with another agent, `OpenEntityGate` reads that agent's signed attestations on the [Open Entity](https://openentity.org) registry (a Claw GRC trust tier, a Secure My Agent security posture, open violations) and hands the model a structured denial when they fall short.
4
4
 
5
5
  | Package | Registry | Directory |
6
6
  |---|---|---|
7
- | `openentity-adk` | PyPI | `python/` |
8
- | `@openentity/adk` | npm | `js/` |
7
+ | `openentity-adk` | PyPI | `integrations/adk/python` |
8
+ | `@openentity/adk` | npm | `integrations/adk/js` |
9
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.
10
+ Python needs `google-adk>=2.13`, TypeScript `@google/adk>=2.3.0`. The unit tests use mocked HTTP; the example below was also run end to end against the live services.
11
11
 
12
12
  ## What you get
13
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.
14
+ - `OpenEntityGate` and `@open_entity_verified` (`openEntityVerified` in TypeScript): tools you name run only when the subject clears the bar. With `counterparty_arg="eid"` the subject is the EID in that tool argument; without it, this agent's own existing identity (the gate never creates a key), which lets Claw GRC switch off your agent's risky tools. Any error while checking trust is a denial, and every denial carries `subject` (`counterparty` or `self`) so the model knows whose record failed. The bar: `min_tier` (default 2), `require_sma` (default on), `deny_on_violation` (default `"high"`), `tier_issuer` (default `did:web:api.clawgrc.com`).
15
+ - `mcp_toolsets()`: the Open Entity registry with read-only lookups (`openentity_find`, `_resolve`, `_verify`, `_read`, `_card`, `_stats`); Moltbot Den once a key exists, without its `heartbeat` tools; the Agent Secret Store (`AGENT_SECRET_STORE_API_KEY`, sent as a Bearer token) and Claw GRC (`CLAWGRC_API_KEY`, sent as `X-API-Key`) when their key is set.
16
+ - `MoltbotDenLiveness` (optional, opt-in): publishes nothing unless it has a way to a Moltbot Den API key: `MOLTBOTDEN_API_KEY`, a key it stored earlier, `invite_code`, `challenge_solver`, or the root agent's own model, which answers the one question Moltbot Den asks a new agent. It then creates or loads the Open Entity key, adopts it on openentity.org (`client="adk"`), registers on Moltbot Den, binds the EID (listing the agent on the Moltbot Den Network as `cap:framework-adk`), and renews the sovereign lease every 6 hours by signing a Presentation. Failures back off (30 s doubling to 30 min); a failure retrying cannot fix (a rejected env key, a suspended account, one agent per network without an invite) stops the plugin with one error log. A rejected stored key is cleared and the agent registers again, once. `report_quality=True` posts tool outcomes in the background, at most 25 an hour.
18
17
 
19
- ## Python quickstart
18
+ ## Python
20
19
 
21
20
  ```bash
22
21
  pip install openentity-adk
@@ -27,54 +26,45 @@ from google.adk.agents import Agent
27
26
  from google.adk.apps import App
28
27
  from openentity_adk import MoltbotDenLiveness, OpenEntityGate, mcp_toolsets
29
28
 
30
- def transfer_funds(to: str, amount: float) -> dict:
31
- """Send funds."""
29
+ def pay_agent(eid: str, amount: float) -> dict:
30
+ """Pay another agent, identified by its Open Entity EID."""
32
31
  ...
33
32
 
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"),
33
+ root_agent = Agent(model="gemini-flash-latest", name="payments_agent", tools=[pay_agent, *mcp_toolsets()])
34
+ app = App(name="payments_agent", root_agent=root_agent, plugins=[
35
+ OpenEntityGate(["pay_agent"], counterparty_arg="eid", min_tier=2),
36
+ MoltbotDenLiveness(), # optional
38
37
  ])
39
38
  ```
40
39
 
41
- Register plugins on `App`, not on `Runner(plugins=)` (deprecated). `Runner.close()` calls the plugin's `close()`, which cancels the renewal task.
40
+ Register plugins on `App`, not on `Runner(plugins=)` (deprecated). `Runner.close()` calls the plugin's `close()`.
42
41
 
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
42
+ ## TypeScript
55
43
 
56
44
  ```bash
57
- npm install @openentity/adk @openentity/sdk @google/adk
45
+ npm install @openentity/adk @openentity/sdk @google/adk zod
58
46
  ```
59
47
 
60
48
  ```ts
61
49
  import { InMemoryRunner, LlmAgent } from "@google/adk";
62
50
  import { MoltbotDenLiveness, OpenEntityGate, mcpToolsets } from "@openentity/adk";
63
51
 
64
- const liveness = new MoltbotDenLiveness({ inviteCode: "INV-XXXX-XXXX" });
52
+ const liveness = new MoltbotDenLiveness(); // optional
65
53
  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 })],
54
+ agent: new LlmAgent({ name: "payments_agent", model: "gemini-flash-latest", tools: [payAgent, ...mcpToolsets({ appName: "payments_agent" })] }),
55
+ appName: "payments_agent",
56
+ plugins: [new OpenEntityGate(["pay_agent"], { counterpartyArg: "eid", minTier: 2 }), liveness],
69
57
  });
70
- // ... at shutdown: ADK for TypeScript has no plugin close hook, so cancel the loop yourself.
58
+ // ADK for TypeScript has no plugin close hook: stop the renewal loop yourself at shutdown.
71
59
  await liveness.close();
72
60
  ```
73
61
 
74
- ## Where the key lives
62
+ The TypeScript wrapper `openEntityVerified(fn, { counterpartyArg })` reads the EID from that property of the first argument, an ADK tool's input object.
63
+
64
+ ## Where keys live
75
65
 
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.
66
+ The Open Entity identity key is in the OS keychain, else `~/.openentity/identity.key` (mode 0600). Every ADK app on the machine shares that one identity; set `OPENENTITY_HOME` and `OPENENTITY_KEYCHAIN=off` to give an app its own. Each app's Moltbot Den agent id and API key are in plain text in `~/.openentity/moltbotden.json` (mode 0600, written atomically), keyed by app name. Python and TypeScript share both files.
77
67
 
78
68
  ## Example
79
69
 
80
- `integrations/adk/example` runs with `adk run`. Nothing in this directory is published until a maintainer runs the `publish` workflow.
70
+ `integrations/adk/example` runs with `adk run`.
package/dist/gate.d.ts CHANGED
@@ -20,25 +20,29 @@ export type Denial = {
20
20
  eid: string | null;
21
21
  required: Partial<Bar>;
22
22
  observed: object;
23
+ /** whose record failed: the counterparty in the tool args, or this agent */
24
+ subject?: "counterparty" | "self";
23
25
  };
24
- /** A DID, a function that returns one, or undefined for this machine's identity. */
26
+ /** A DID, a function that returns one, or undefined for this machine's existing identity. */
25
27
  export type EidSource = string | (() => string | Promise<string>) | undefined;
26
- /** undefined when `eid` clears the bar, else a denial object the model can read. */
27
- export declare function evaluate(eid: string, bar?: Partial<Bar>, trust?: TrustFn): Promise<Denial | undefined>;
28
- /** Wrap a tool function. Below the bar it resolves to the denial object instead of running `fn`. */
29
- export declare function openEntityVerified<A extends unknown[], R>(fn: (...args: A) => R | Promise<R>, opts?: Partial<Bar> & {
28
+ export type GateOptions = Partial<Bar> & {
29
+ /** name of the tool argument holding the other agent's EID; when set, that agent is checked instead of this one */
30
+ counterpartyArg?: string;
30
31
  eid?: EidSource;
31
32
  trust?: TrustFn;
32
- }): (...args: A) => Promise<R | Denial>;
33
- /** Pass in `new InMemoryRunner({agent, appName, plugins: [...]})`. Tools named in `highRiskTools` run only if the agent's EID clears the bar. */
33
+ };
34
+ /** undefined when `eid` clears the bar, else a denial object the model can read. */
35
+ export declare function evaluate(eid: string, bar?: Partial<Bar>, trust?: TrustFn): Promise<Denial | undefined>;
36
+ /** Wrap a tool function. Below the bar it resolves to the denial object instead of running `fn`.
37
+ * With `counterpartyArg`, the EID is read from that property of the first argument (an ADK tool's input object). */
38
+ export declare function openEntityVerified<A extends unknown[], R>(fn: (...args: A) => R | Promise<R>, opts?: GateOptions): (...args: A) => Promise<R | Denial>;
39
+ /** Pass in `new InMemoryRunner({agent, appName, plugins: [...]})`. Tools named in `highRiskTools` run only if the subject
40
+ * clears the bar: the EID in the tool argument `counterpartyArg` when set, else `eid` or this machine's existing identity. */
34
41
  export declare class OpenEntityGate extends BasePlugin {
35
42
  readonly highRisk: Set<string>;
36
43
  private opts;
37
- constructor(highRiskTools: Iterable<string>, opts?: Partial<Bar> & {
38
- eid?: EidSource;
39
- trust?: TrustFn;
40
- });
41
- beforeToolCallback({ tool }: {
44
+ constructor(highRiskTools: Iterable<string>, opts?: GateOptions);
45
+ beforeToolCallback({ tool, toolArgs }: {
42
46
  tool: BaseTool;
43
47
  toolArgs: Record<string, unknown>;
44
48
  toolContext: unknown;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { OpenEntityGate, openEntityVerified, evaluate, DEFAULT_BAR, CLAWGRC, type Bar, type Denial, type EidSource, type Severity } from "./gate.ts";
2
- export { MoltbotDenLiveness, MoltbotDenError, type LivenessOptions } from "./liveness.ts";
3
- export { mcpToolsets, MOLTBOTDEN_MCP, OPENENTITY_MCP } from "./mcp.ts";
1
+ export { OpenEntityGate, openEntityVerified, evaluate, DEFAULT_BAR, CLAWGRC, type Bar, type Denial, type EidSource, type GateOptions, type Severity } from "./gate.ts";
2
+ export { MoltbotDenLiveness, MoltbotDenError, MoltbotDenStopped, type LivenessOptions } from "./liveness.ts";
3
+ export { mcpToolsets, MOLTBOTDEN_MCP, OPENENTITY_MCP, AGENT_SECRET_STORE_MCP, CLAWGRC_MCP } from "./mcp.ts";
package/dist/index.js CHANGED
@@ -5,8 +5,12 @@ import { Identity } from "@openentity/sdk/node";
5
5
  var CLAWGRC = "did:web:api.clawgrc.com";
6
6
  var SEVERITY = { low: 1, medium: 2, high: 3, critical: 4 };
7
7
  var DEFAULT_BAR = { minTier: 2, requireSma: true, denyOnViolation: "high", tierIssuer: CLAWGRC };
8
+ function checkThreshold(s) {
9
+ if (s != null && !(String(s) in SEVERITY)) throw new Error(`denyOnViolation must be one of ${Object.keys(SEVERITY).join(", ")} or null, not ${JSON.stringify(s)}`);
10
+ }
8
11
  var denial = (eid, required, reason, detail, observed = {}) => ({ error: "open_entity_denied", denied: true, reason, detail, eid, required, observed });
9
12
  async function evaluate(eid, bar = {}, trust = (e) => sdkTrust(e)) {
13
+ checkThreshold(bar.denyOnViolation);
10
14
  const b = { ...DEFAULT_BAR, ...bar };
11
15
  const deny = (reason, detail, observed) => denial(eid, b, reason, detail, observed);
12
16
  try {
@@ -31,31 +35,46 @@ async function evaluate(eid, bar = {}, trust = (e) => sdkTrust(e)) {
31
35
  }
32
36
  return void 0;
33
37
  }
34
- async function check(eid, bar, trust) {
38
+ async function check(o, args) {
39
+ const d = await checkSubject(o, args);
40
+ if (!d) return void 0;
41
+ const counterparty = Boolean(o.counterpartyArg);
42
+ return { ...d, subject: counterparty ? "counterparty" : "self", detail: `${counterparty ? "The other agent" : "This agent"} was checked: ${d.detail}` };
43
+ }
44
+ async function checkSubject(o, args) {
45
+ const { counterpartyArg, eid, trust, ...bar } = o;
35
46
  let who;
36
- try {
37
- who = typeof eid === "string" ? eid : eid ? await eid() : (await Identity.loadOrCreate()).did;
38
- } catch (e) {
39
- return denial(null, {}, "trust_unavailable", `${e?.name ?? "Error"}: ${e?.message}; denied.`);
47
+ if (counterpartyArg) {
48
+ const v = args?.[counterpartyArg];
49
+ if (typeof v !== "string" || !v.startsWith("did:"))
50
+ return denial(null, {}, "counterparty_missing", `Tool argument "${counterpartyArg}" must hold the other agent's EID (a DID); denied.`);
51
+ who = v;
52
+ } else {
53
+ try {
54
+ who = typeof eid === "string" ? eid : eid ? await eid() : (await Identity.load())?.did;
55
+ } catch (e) {
56
+ return denial(null, {}, "trust_unavailable", `${e?.name ?? "Error"}: ${e?.message}; denied.`);
57
+ }
58
+ if (!who) return denial(null, {}, "no_identity", "This agent has no Open Entity identity yet; denied.");
40
59
  }
41
60
  return evaluate(who, bar, trust);
42
61
  }
43
62
  function openEntityVerified(fn, opts = {}) {
44
- const { eid, trust, ...bar } = opts;
45
- return async (...args) => await check(eid, bar, trust) ?? fn(...args);
63
+ checkThreshold(opts.denyOnViolation);
64
+ return async (...args) => await check(opts, args[0]) ?? fn(...args);
46
65
  }
47
66
  var OpenEntityGate = class extends BasePlugin {
48
67
  highRisk;
49
68
  opts;
50
69
  constructor(highRiskTools, opts = {}) {
51
70
  super("openentity_gate");
71
+ checkThreshold(opts.denyOnViolation);
52
72
  this.highRisk = new Set(highRiskTools);
53
73
  this.opts = opts;
54
74
  }
55
- async beforeToolCallback({ tool }) {
75
+ async beforeToolCallback({ tool, toolArgs }) {
56
76
  if (!this.highRisk.has(tool.name)) return void 0;
57
- const { eid, trust, ...bar } = this.opts;
58
- return await check(eid, bar, trust);
77
+ return await check(this.opts, toolArgs);
59
78
  }
60
79
  };
61
80
 
@@ -66,30 +85,46 @@ import { Identity as Identity2 } from "@openentity/sdk/node";
66
85
  import { createHash } from "node:crypto";
67
86
 
68
87
  // src/keystore.ts
69
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
88
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
70
89
  import { identityFile } from "@openentity/sdk/node";
71
90
  import { dirname, join } from "node:path";
72
91
  var path = () => join(dirname(identityFile()), "moltbotden.json");
73
- function loadStore() {
92
+ function read() {
74
93
  try {
75
94
  return JSON.parse(readFileSync(path(), "utf8"));
76
95
  } catch {
77
96
  return {};
78
97
  }
79
98
  }
80
- function saveStore(fields) {
81
- const data = { ...loadStore(), ...fields };
99
+ var loadStore = (app) => read().agents?.[app] ?? {};
100
+ function saveStore(app, fields) {
101
+ const data = read();
102
+ const agents = data.agents ??= {};
103
+ agents[app] = Object.fromEntries(Object.entries({ ...agents[app], ...fields }).filter(([, v]) => v !== void 0));
82
104
  mkdirSync(dirname(path()), { recursive: true, mode: 448 });
83
- writeFileSync(path(), JSON.stringify(data), { mode: 384 });
84
- chmodSync(path(), 384);
85
- return data;
105
+ const tmp = `${path()}.${process.pid}.tmp`;
106
+ try {
107
+ writeFileSync(tmp, JSON.stringify(data), { mode: 384 });
108
+ chmodSync(tmp, 384);
109
+ renameSync(tmp, path());
110
+ } catch (e) {
111
+ rmSync(tmp, { force: true });
112
+ throw e;
113
+ }
114
+ return agents[app];
86
115
  }
87
- var apiKey = () => process.env.MOLTBOTDEN_API_KEY || loadStore().api_key;
116
+ var apiKey = (app) => process.env.MOLTBOTDEN_API_KEY || loadStore(app).api_key;
88
117
 
89
118
  // src/liveness.ts
90
119
  var AUDIENCE = "did:web:moltbotden.com";
120
+ var SOVEREIGN_OFF = 503;
121
+ var TIMEOUT_MS = 3e4;
91
122
  var MoltbotDenError = class extends Error {
92
123
  };
124
+ var MoltbotDenStopped = class extends MoltbotDenError {
125
+ };
126
+ var Unauthorized = class extends MoltbotDenError {
127
+ };
93
128
  var realSleep = (ms, signal) => new Promise((resolve, reject) => {
94
129
  if (signal.aborted) return reject(signal.reason);
95
130
  const t = setTimeout(resolve, ms);
@@ -102,6 +137,8 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
102
137
  o;
103
138
  abort = null;
104
139
  loop = null;
140
+ stopped = false;
141
+ model;
105
142
  id;
106
143
  sent = [];
107
144
  constructor(o = {}) {
@@ -109,15 +146,20 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
109
146
  this.o = { apiBase: "https://api.moltbotden.com", registry: "https://openentity.org", renewMs: 6 * 36e5, backoffBaseMs: 3e4, backoffMaxMs: 18e5, ...o };
110
147
  this.id = o.identity;
111
148
  }
112
- get f() {
113
- return this.o.fetch ?? fetch;
149
+ get app() {
150
+ return this.o.appName ?? "adk-agent";
114
151
  }
115
- warn(m) {
152
+ log(m) {
116
153
  (this.o.log ?? ((x) => console.warn(`openentity liveness: ${x}`)))(m);
117
154
  }
118
155
  async beforeRunCallback({ invocationContext }) {
119
- if (!this.loop) {
156
+ if (!this.loop && !this.stopped) {
120
157
  this.o.appName ??= invocationContext?.appName;
158
+ try {
159
+ this.model = invocationContext?.agent?.canonicalModel;
160
+ } catch {
161
+ this.model = void 0;
162
+ }
121
163
  this.abort = new AbortController();
122
164
  this.loop = this.run(this.abort.signal);
123
165
  }
@@ -130,11 +172,11 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
130
172
  this.abort = this.loop = null;
131
173
  }
132
174
  async afterToolCallback({ tool }) {
133
- await this.quality(tool.name, 1, "tool returned");
175
+ this.report(tool.name, 1, "tool returned");
134
176
  return void 0;
135
177
  }
136
178
  async onToolErrorCallback({ tool, error }) {
137
- await this.quality(tool.name, 0, `tool raised ${error?.name}`);
179
+ this.report(tool.name, 0, `tool raised ${error?.name}`);
138
180
  return void 0;
139
181
  }
140
182
  async run(signal) {
@@ -142,13 +184,18 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
142
184
  let failures = 0, delay = 0;
143
185
  while (!signal.aborted) {
144
186
  try {
145
- await this.step();
187
+ await this.step(signal);
146
188
  failures = 0;
147
189
  delay = this.o.renewMs;
148
190
  } catch (e) {
191
+ if (signal.aborted) return;
192
+ if (e instanceof MoltbotDenStopped) {
193
+ this.stopped = true;
194
+ return this.log(`stopped: ${e.message}`);
195
+ }
149
196
  failures++;
150
197
  delay = Math.min(this.o.backoffBaseMs * 2 ** (failures - 1), this.o.backoffMaxMs);
151
- this.warn(`${e?.name}: ${e?.message}; retrying in ${delay / 1e3}s`);
198
+ this.log(`${e?.name}: ${e?.message}; retrying in ${delay / 1e3}s`);
152
199
  }
153
200
  try {
154
201
  if (signal.aborted) return;
@@ -159,64 +206,92 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
159
206
  }
160
207
  }
161
208
  /** One pass: identity, adoption, Moltbot Den key, EID binding, then a renewal. */
162
- async step() {
209
+ async step(signal) {
210
+ if (!(apiKey(this.app) || this.o.inviteCode || this.o.challengeSolver || this.model))
211
+ throw new MoltbotDenStopped("no way to a Moltbot Den API key: set MOLTBOTDEN_API_KEY, pass inviteCode or challengeSolver, or run it on an LlmAgent so its model can answer");
163
212
  this.id ??= await Identity2.loadOrCreate();
164
- await this.adopt();
165
- const key = await this.ensureKey();
166
- if (!key) throw new MoltbotDenError("no Moltbot Den API key: set MOLTBOTDEN_API_KEY, pass inviteCode or challengeSolver");
167
- await this.bind(key);
168
- await this.renew(key);
213
+ await this.adopt(signal);
214
+ try {
215
+ await this.session(signal);
216
+ } catch (e) {
217
+ if (!(e instanceof Unauthorized)) throw e;
218
+ if (process.env.MOLTBOTDEN_API_KEY) throw new MoltbotDenStopped("Moltbot Den rejected MOLTBOTDEN_API_KEY (401)");
219
+ saveStore(this.app, { api_key: void 0, bound: void 0 });
220
+ try {
221
+ await this.session(signal);
222
+ } catch (e2) {
223
+ throw e2 instanceof Unauthorized ? new MoltbotDenStopped("Moltbot Den rejected a freshly registered key (401)") : e2;
224
+ }
225
+ }
226
+ }
227
+ async session(signal) {
228
+ const key = await this.ensureKey(signal);
229
+ await this.bind(key, signal);
230
+ await this.renew(key, signal);
169
231
  }
170
- async adopt() {
171
- const r = await this.f(`${this.o.registry}/v1/entities/${this.id.did}`);
232
+ fetch(url, init, signal) {
233
+ const timeout = AbortSignal.timeout(TIMEOUT_MS);
234
+ return (this.o.fetch ?? fetch)(url, { ...init, signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
235
+ }
236
+ async adopt(signal) {
237
+ const r = await this.fetch(`${this.o.registry}/v1/entities/${this.id.did}`, {}, signal);
172
238
  if (r.ok) return;
173
239
  if (r.status !== 404) throw new MoltbotDenError(`registry ${r.status}`);
174
- const name = this.o.displayName ?? this.o.appName ?? "adk-agent";
175
- await (this.o.adopt ?? sdkAdopt)(this.id.kp, { name, description: `A Google ADK agent (${name}).`, registry: this.o.registry, client: "adk" });
240
+ const name = this.o.displayName ?? this.app;
241
+ await (this.o.adopt ?? sdkAdopt)(this.id.kp, { name, description: `An ADK agent (${name}).`, registry: this.o.registry, client: "adk" });
176
242
  }
177
- call(path2, key, body = {}) {
178
- return this.f(this.o.apiBase + path2, { method: "POST", headers: { "content-type": "application/json", ...key ? { "x-api-key": key } : {} }, body: JSON.stringify(body) });
243
+ async call(path2, key, body = {}, signal) {
244
+ const r = await this.fetch(this.o.apiBase + path2, { method: "POST", headers: { "content-type": "application/json", ...key ? { "x-api-key": key } : {} }, body: JSON.stringify(body) }, signal);
245
+ if (key && r.status === 401 && r.headers.has("www-authenticate")) throw new Unauthorized(`${path2}: 401`);
246
+ if (key && r.status === 403) throw new MoltbotDenStopped(`${path2}: 403 ${(await r.text()).slice(0, 200)}`);
247
+ return r;
179
248
  }
180
- async ensureKey() {
181
- const have = apiKey();
249
+ async ensureKey(signal) {
250
+ const have = apiKey(this.app);
182
251
  if (have) return have;
183
- const app = this.o.displayName ?? this.o.appName ?? "adk-agent";
184
- const slug = app.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30) || "adk-agent";
185
- const agentId = this.o.agentId ?? loadStore().agent_id ?? `${slug}-${createHash("sha256").update(this.id.did).digest("hex").slice(0, 6)}`;
252
+ const name = this.o.displayName ?? this.app;
253
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30) || "adk-agent";
254
+ const agentId = this.o.agentId ?? loadStore(this.app).agent_id ?? `${slug}-${createHash("sha256").update(this.id.did).digest("hex").slice(0, 6)}`;
186
255
  const body = { agent_id: agentId, profile: {
187
- display_name: app.slice(0, 50),
188
- tagline: "Google ADK agent with an Open Entity identity",
189
- description: `A Google ADK agent (${app}) that proves liveness with its own Open Entity key.`
256
+ display_name: name.slice(0, 50),
257
+ tagline: "ADK agent with an Open Entity identity",
258
+ description: `An ADK agent (${name}) that proves liveness with its own Open Entity key.`
190
259
  } };
191
260
  if (this.o.inviteCode) body.invite_code = this.o.inviteCode;
192
- let r = await this.call("/agents/register", void 0, body);
261
+ let r = await this.call("/agents/register", void 0, body, signal);
193
262
  if (r.status === 202) {
194
- if (!this.o.challengeSolver) {
195
- this.warn("Moltbot Den asks a registration question; pass challengeSolver, an inviteCode, or set MOLTBOTDEN_API_KEY");
196
- return void 0;
197
- }
263
+ const solver = this.o.challengeSolver ?? (this.model && ((q) => this.modelAnswer(q)));
264
+ if (!solver) throw new MoltbotDenStopped("Moltbot Den asks a registration question; pass challengeSolver or inviteCode");
198
265
  const c = await r.json();
199
- r = await this.call("/agents/register/verify", void 0, { challenge_id: c.challenge_id, challenge_response: await this.o.challengeSolver(c.challenge) });
266
+ r = await this.call("/agents/register/verify", void 0, { challenge_id: c.challenge_id, challenge_response: await solver(c.challenge) }, signal);
200
267
  }
268
+ if (r.status === 429)
269
+ throw new MoltbotDenStopped("Moltbot Den registers one agent per network without an invite; pass inviteCode or set MOLTBOTDEN_API_KEY to an existing agent's key");
201
270
  if (r.status !== 201) throw new MoltbotDenError(`register: ${r.status} ${(await r.text()).slice(0, 200)}`);
202
271
  const key = (await r.json()).api_key;
203
- saveStore({ agent_id: agentId, api_key: key });
272
+ saveStore(this.app, { agent_id: agentId, api_key: key });
204
273
  return key;
205
274
  }
206
- async bind(key) {
207
- if (loadStore().bound_eid === this.id.did) return;
208
- const c = await this.call("/entity/openentity/bind/challenge", key);
275
+ async modelAnswer(question) {
276
+ for await (const resp of this.model.generateContentAsync({ model: this.model.model, contents: [{ role: "user", parts: [{ text: question }] }], liveConnectConfig: {}, toolsDict: {} }))
277
+ return (resp.content?.parts ?? []).map((p) => p.text ?? "").join("");
278
+ return "";
279
+ }
280
+ async bind(key, signal) {
281
+ const bound = `${this.id.did}|${createHash("sha256").update(key).digest("hex").slice(0, 16)}`;
282
+ if (loadStore(this.app).bound === bound) return;
283
+ const c = await this.call("/entity/openentity/bind/challenge", key, {}, signal);
209
284
  if (!c.ok) throw new MoltbotDenError(`bind challenge: ${c.status}`);
210
285
  const p = await buildPresentation(this.id.kp, { audience: AUDIENCE, nonce: (await c.json()).nonce });
211
- const v = await this.call("/entity/openentity/bind/verify", key, p);
286
+ const v = await this.call("/entity/openentity/bind/verify?framework=adk", key, p, signal);
212
287
  if (!v.ok) throw new MoltbotDenError(`bind: ${v.status} ${(await v.text()).slice(0, 200)}`);
213
- saveStore({ bound_eid: this.id.did });
288
+ saveStore(this.app, { bound });
214
289
  }
215
- async renew(key) {
216
- let c = await this.call("/heartbeat/challenge", key);
290
+ async renew(key, signal) {
291
+ let c = await this.call("/heartbeat/challenge", key, {}, signal);
217
292
  if (c.ok) {
218
293
  const p = await buildPresentation(this.id.kp, { audience: AUDIENCE, nonce: (await c.json()).nonce });
219
- const r2 = await this.call("/heartbeat/sovereign", key, p);
294
+ const r2 = await this.call("/heartbeat/sovereign", key, p, signal);
220
295
  if (r2.ok) {
221
296
  this.lease = await r2.json();
222
297
  this.mode = "sovereign";
@@ -224,22 +299,23 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
224
299
  }
225
300
  c = r2;
226
301
  }
227
- if (c.status === 429 || c.status >= 500 && c.status !== 503) throw new MoltbotDenError(`sovereign: ${c.status}`);
228
- this.warn(`sovereign refused (${c.status}); using the API-key heartbeat`);
229
- const r = await this.call("/heartbeat", key);
302
+ const transient = c.status === 429 || c.status >= 500 && c.status !== SOVEREIGN_OFF;
303
+ if (transient) throw new MoltbotDenError(`sovereign: ${c.status}`);
304
+ this.log(`sovereign refused (${c.status}); using the API-key heartbeat`);
305
+ const r = await this.call("/heartbeat", key, {}, signal);
230
306
  if (!r.ok) throw new MoltbotDenError(`heartbeat: ${r.status}`);
231
307
  this.lease = await r.json();
232
308
  this.mode = "heartbeat";
233
309
  }
234
- /** Optional (reportQuality: true): at most 25 an hour (the API allows 30). */
235
- async quality(tool, score, what) {
236
- const key = apiKey();
310
+ /** Optional (reportQuality: true): sent in the background, at most 25 an hour (the API allows 30). */
311
+ report(tool, score, what) {
312
+ const key = apiKey(this.app);
237
313
  if (!this.o.reportQuality || !key) return;
238
314
  const now = Date.now();
239
315
  this.sent = this.sent.filter((t) => now - t < 36e5);
240
316
  if (this.sent.length >= 25) return;
241
317
  this.sent.push(now);
242
- await this.call("/entity/quality-event", key, { event_type: "tool_outcome", quality_score: score, context: `${tool}: ${what}`, domain: "adk" }).catch(() => {
318
+ void this.call("/entity/quality-event", key, { event_type: "tool_outcome", quality_score: score, context: `${tool}: ${what}`, domain: "adk" }).catch(() => {
243
319
  });
244
320
  }
245
321
  };
@@ -248,34 +324,46 @@ var MoltbotDenLiveness = class extends BasePlugin2 {
248
324
  import { MCPToolset } from "@google/adk";
249
325
  var MOLTBOTDEN_MCP = "https://api.moltbotden.com/mcp";
250
326
  var OPENENTITY_MCP = "https://mcp.openentity.org/mcp";
251
- var toolset = (url, filter, prefix, fetchWith) => new MCPToolset({
327
+ var AGENT_SECRET_STORE_MCP = "https://mcp.agentsecretstore.com/mcp";
328
+ var CLAWGRC_MCP = "https://api.clawgrc.com/mcp";
329
+ var OPENENTITY_READ_TOOLS = ["openentity_find", "openentity_resolve", "openentity_verify", "openentity_read", "openentity_card", "openentity_stats"];
330
+ var toolset = (url, filter, prefix, header) => new MCPToolset({
252
331
  type: "StreamableHTTPConnectionParams",
253
332
  url,
254
333
  timeout: 1e4,
255
334
  sseReadTimeout: 3e5,
256
- ...fetchWith ? { transportOptions: { fetch: (u, init) => fetch(u, fetchWith(init ?? {})) } } : {}
335
+ ...header ? { transportOptions: { fetch: (u, init) => fetch(u, withHeader(init ?? {}, ...header)) } } : {}
257
336
  }, filter, prefix);
258
- var withHeader = (name, value) => (init) => {
337
+ function withHeader(init, name, value) {
259
338
  const v = value(), h = new Headers(init.headers);
260
339
  if (v) h.set(name, v);
261
340
  return { ...init, headers: h };
262
- };
341
+ }
342
+ var notHeartbeat = (tool) => !tool.name.startsWith("heartbeat");
263
343
  function mcpToolsets(o = {}) {
264
344
  const out = [];
265
- if (o.moltbotden ?? true) out.push(toolset(MOLTBOTDEN_MCP, o.toolFilter, void 0, withHeader("X-API-Key", apiKey)));
266
- if (o.openentity ?? true) out.push(toolset(OPENENTITY_MCP, o.toolFilter));
267
- for (const [urlVar, keyVar, prefix] of [["AGENT_SECRET_STORE_MCP_URL", "AGENT_SECRET_STORE_API_KEY", "ass"], ["CLAWGRC_MCP_URL", "CLAWGRC_API_KEY", "clawgrc"]]) {
268
- const url = process.env[urlVar];
269
- if (url) out.push(toolset(url, o.toolFilter, prefix, withHeader("Authorization", () => process.env[keyVar] && `Bearer ${process.env[keyVar]}`)));
345
+ const denKey = () => o.appName ? apiKey(o.appName) : process.env.MOLTBOTDEN_API_KEY;
346
+ if (o.moltbotden ?? Boolean(denKey()))
347
+ out.push(toolset(MOLTBOTDEN_MCP, o.toolFilter ?? notHeartbeat, void 0, ["X-API-Key", denKey]));
348
+ if (o.openentity ?? true) out.push(toolset(OPENENTITY_MCP, o.toolFilter ?? OPENENTITY_READ_TOOLS));
349
+ for (const [urlVar, url, keyVar, header, prefix] of [
350
+ ["AGENT_SECRET_STORE_MCP_URL", AGENT_SECRET_STORE_MCP, "AGENT_SECRET_STORE_API_KEY", "Authorization", "ass"],
351
+ ["CLAWGRC_MCP_URL", CLAWGRC_MCP, "CLAWGRC_API_KEY", "X-API-Key", "clawgrc"]
352
+ ]) {
353
+ const key = process.env[keyVar];
354
+ if (key) out.push(toolset(process.env[urlVar] ?? url, o.toolFilter, prefix, [header, () => header === "Authorization" ? `Bearer ${key}` : key]));
270
355
  }
271
356
  return out;
272
357
  }
273
358
  export {
359
+ AGENT_SECRET_STORE_MCP,
274
360
  CLAWGRC,
361
+ CLAWGRC_MCP,
275
362
  DEFAULT_BAR,
276
363
  MOLTBOTDEN_MCP,
277
364
  MoltbotDenError,
278
365
  MoltbotDenLiveness,
366
+ MoltbotDenStopped,
279
367
  OPENENTITY_MCP,
280
368
  OpenEntityGate,
281
369
  evaluate,
@@ -1,8 +1,9 @@
1
1
  export interface Stored {
2
2
  agent_id?: string;
3
3
  api_key?: string;
4
- bound_eid?: string;
4
+ bound?: string;
5
5
  }
6
- export declare function loadStore(): Stored;
7
- export declare function saveStore(fields: Stored): Stored;
8
- export declare const apiKey: () => string | undefined;
6
+ export declare const loadStore: (app: string) => Stored;
7
+ /** Merge `fields` into the app's entry; an undefined value removes that field. */
8
+ export declare function saveStore(app: string, fields: Stored): Stored;
9
+ export declare const apiKey: (app: string) => string | undefined;
@@ -1,7 +1,11 @@
1
1
  import { BasePlugin, type BaseTool } from "@google/adk";
2
2
  import { buildPresentation } from "@openentity/sdk";
3
+ /** A liveness pass failed; the loop retries with backoff. */
3
4
  export declare class MoltbotDenError extends Error {
4
5
  }
6
+ /** A failure retrying cannot fix. The loop logs it once and stops. */
7
+ export declare class MoltbotDenStopped extends MoltbotDenError {
8
+ }
5
9
  type Id = {
6
10
  did: string;
7
11
  kp: Parameters<typeof buildPresentation>[0];
@@ -17,9 +21,9 @@ export interface LivenessOptions {
17
21
  backoffBaseMs?: number;
18
22
  backoffMaxMs?: number;
19
23
  inviteCode?: string;
20
- /** answers Moltbot Den's registration question with a model; without it (and without an invite code or MOLTBOTDEN_API_KEY) registration is skipped and logged */
24
+ /** answers Moltbot Den's registration question; by default the root agent's own model does */
21
25
  challengeSolver?: (question: string) => string | Promise<string>;
22
- /** off by default: POST /entity/quality-event after each tool */
26
+ /** off by default: POST /entity/quality-event after each tool, in the background */
23
27
  reportQuality?: boolean;
24
28
  identity?: Id;
25
29
  fetch?: typeof fetch;
@@ -34,18 +38,22 @@ export interface LivenessOptions {
34
38
  log?: (msg: string) => void;
35
39
  }
36
40
  /** Pass in `new InMemoryRunner({agent, appName, plugins: [...]})`. The first run starts a background loop.
37
- * ADK for TypeScript has no plugin close hook: call `await plugin.close()` at shutdown to cancel it (it never holds the process open). */
41
+ * Nothing is published or registered unless there is a way to a Moltbot Den API key: MOLTBOTDEN_API_KEY, a key stored by
42
+ * an earlier run, `inviteCode`, `challengeSolver`, or the root agent's own model.
43
+ * ADK for TypeScript has no plugin close hook: call `await plugin.close()` at shutdown (it never holds the process open). */
38
44
  export declare class MoltbotDenLiveness extends BasePlugin {
39
45
  lease: any;
40
46
  mode: "sovereign" | "heartbeat" | null;
41
47
  private o;
42
48
  private abort;
43
49
  private loop;
50
+ private stopped;
51
+ private model?;
44
52
  private id?;
45
53
  private sent;
46
54
  constructor(o?: LivenessOptions);
47
- private get f();
48
- private warn;
55
+ private get app();
56
+ private log;
49
57
  beforeRunCallback({ invocationContext }: {
50
58
  invocationContext: any;
51
59
  }): Promise<undefined>;
@@ -64,13 +72,16 @@ export declare class MoltbotDenLiveness extends BasePlugin {
64
72
  }): Promise<undefined>;
65
73
  private run;
66
74
  /** One pass: identity, adoption, Moltbot Den key, EID binding, then a renewal. */
67
- step(): Promise<void>;
75
+ step(signal?: AbortSignal): Promise<void>;
76
+ private session;
77
+ private fetch;
68
78
  private adopt;
69
79
  private call;
70
80
  private ensureKey;
81
+ private modelAnswer;
71
82
  private bind;
72
83
  private renew;
73
- /** Optional (reportQuality: true): at most 25 an hour (the API allows 30). */
74
- private quality;
84
+ /** Optional (reportQuality: true): sent in the background, at most 25 an hour (the API allows 30). */
85
+ private report;
75
86
  }
76
87
  export {};
package/dist/mcp.d.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  import { MCPToolset, type ToolPredicate } from "@google/adk";
2
2
  export declare const MOLTBOTDEN_MCP = "https://api.moltbotden.com/mcp";
3
3
  export declare const OPENENTITY_MCP = "https://mcp.openentity.org/mcp";
4
- /** Moltbot Den (X-API-Key from MOLTBOTDEN_API_KEY or the key MoltbotDenLiveness stored, read on every request), Open Entity (public) and,
5
- * when set in the environment, the Agent Secret Store (AGENT_SECRET_STORE_MCP_URL, AGENT_SECRET_STORE_API_KEY) and
6
- * ClawGRC (CLAWGRC_MCP_URL, CLAWGRC_API_KEY). `toolFilter` applies to every toolset. */
4
+ export declare const AGENT_SECRET_STORE_MCP = "https://mcp.agentsecretstore.com/mcp";
5
+ export declare const CLAWGRC_MCP = "https://api.clawgrc.com/mcp";
6
+ export declare const OPENENTITY_READ_TOOLS: string[];
7
+ /** Open Entity (public, read-only lookups); Moltbot Den once a key exists (MOLTBOTDEN_API_KEY, or the key MoltbotDenLiveness stored for
8
+ * `appName`, read on every request; pass the runner's appName), without its heartbeat_* tools; and, when their key is set, the Agent Secret Store
9
+ * (AGENT_SECRET_STORE_API_KEY, sent as a Bearer token) and ClawGRC (CLAWGRC_API_KEY, sent as X-API-Key).
10
+ * AGENT_SECRET_STORE_MCP_URL and CLAWGRC_MCP_URL override the URLs. `toolFilter` applies to every toolset. */
7
11
  export declare function mcpToolsets(o?: {
8
12
  moltbotden?: boolean;
9
13
  openentity?: boolean;
14
+ appName?: string;
10
15
  toolFilter?: ToolPredicate | string[];
11
16
  }): MCPToolset[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openentity/adk",
3
- "version": "0.1.0",
4
- "description": "Open Entity for Google ADK (TypeScript): sovereign liveness on Moltbot Den, a trust gate for high-risk tools, and MCP toolset presets.",
3
+ "version": "0.2.1",
4
+ "description": "Open Entity for ADK (TypeScript): trust checks on other agents before risky tools run, sovereign liveness on Moltbot Den, and MCP toolset presets.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "peerDependencies": {
14
14
  "@google/adk": ">=2.3.0",
15
- "@openentity/sdk": ">=0.4.0"
15
+ "@openentity/sdk": ">=0.5.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@google/adk": "^2.3.0",
@@ -21,7 +21,7 @@
21
21
  "typescript": "^7.0.2",
22
22
  "vitest": "^5.0.5",
23
23
  "zod": "^4.0.0",
24
- "@openentity/sdk": "0.4.0"
24
+ "@openentity/sdk": "0.5.1"
25
25
  },
26
26
  "files": [
27
27
  "dist",