@openentity/adk 0.0.0-stage → 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 +21 -0
- package/README.md +69 -2
- package/dist/gate.d.ts +51 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +372 -0
- package/dist/keystore.d.ts +9 -0
- package/dist/liveness.d.ts +87 -0
- package/dist/mcp.d.ts +16 -0
- package/package.json +45 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moltbot Den
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Open Entity for ADK
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
5
|
+
| Package | Registry | Directory |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `openentity-adk` | PyPI | `integrations/adk/python` |
|
|
8
|
+
| `@openentity/adk` | npm | `integrations/adk/js` |
|
|
9
|
+
|
|
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
|
+
|
|
12
|
+
## What you get
|
|
13
|
+
|
|
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.
|
|
17
|
+
|
|
18
|
+
## Python
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install openentity-adk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from google.adk.agents import Agent
|
|
26
|
+
from google.adk.apps import App
|
|
27
|
+
from openentity_adk import MoltbotDenLiveness, OpenEntityGate, mcp_toolsets
|
|
28
|
+
|
|
29
|
+
def pay_agent(eid: str, amount: float) -> dict:
|
|
30
|
+
"""Pay another agent, identified by its Open Entity EID."""
|
|
31
|
+
...
|
|
32
|
+
|
|
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
|
|
37
|
+
])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Register plugins on `App`, not on `Runner(plugins=)` (deprecated). `Runner.close()` calls the plugin's `close()`.
|
|
41
|
+
|
|
42
|
+
## TypeScript
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @openentity/adk @openentity/sdk @google/adk zod
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { InMemoryRunner, LlmAgent } from "@google/adk";
|
|
50
|
+
import { MoltbotDenLiveness, OpenEntityGate, mcpToolsets } from "@openentity/adk";
|
|
51
|
+
|
|
52
|
+
const liveness = new MoltbotDenLiveness(); // optional
|
|
53
|
+
const runner = new InMemoryRunner({
|
|
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],
|
|
57
|
+
});
|
|
58
|
+
// ADK for TypeScript has no plugin close hook: stop the renewal loop yourself at shutdown.
|
|
59
|
+
await liveness.close();
|
|
60
|
+
```
|
|
61
|
+
|
|
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
|
|
65
|
+
|
|
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.
|
|
67
|
+
|
|
68
|
+
## Example
|
|
69
|
+
|
|
70
|
+
`integrations/adk/example` runs with `adk run`.
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BasePlugin, type BaseTool } from "@google/adk";
|
|
2
|
+
import { type TrustReport } from "@openentity/sdk";
|
|
3
|
+
export declare const CLAWGRC = "did:web:api.clawgrc.com";
|
|
4
|
+
export type Severity = "low" | "medium" | "high" | "critical";
|
|
5
|
+
export interface Bar {
|
|
6
|
+
minTier: number;
|
|
7
|
+
requireSma: boolean;
|
|
8
|
+
/** deny when a live custom:clawgrc:violation has this severity or worse; null turns the check off */
|
|
9
|
+
denyOnViolation: Severity | null;
|
|
10
|
+
/** whose trust_tier attestation counts; default did:web:api.clawgrc.com */
|
|
11
|
+
tierIssuer: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_BAR: Bar;
|
|
14
|
+
type TrustFn = (eid: string) => Promise<TrustReport>;
|
|
15
|
+
export type Denial = {
|
|
16
|
+
error: "open_entity_denied";
|
|
17
|
+
denied: true;
|
|
18
|
+
reason: string;
|
|
19
|
+
detail: string;
|
|
20
|
+
eid: string | null;
|
|
21
|
+
required: Partial<Bar>;
|
|
22
|
+
observed: object;
|
|
23
|
+
/** whose record failed: the counterparty in the tool args, or this agent */
|
|
24
|
+
subject?: "counterparty" | "self";
|
|
25
|
+
};
|
|
26
|
+
/** A DID, a function that returns one, or undefined for this machine's existing identity. */
|
|
27
|
+
export type EidSource = string | (() => string | Promise<string>) | undefined;
|
|
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;
|
|
31
|
+
eid?: EidSource;
|
|
32
|
+
trust?: TrustFn;
|
|
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. */
|
|
41
|
+
export declare class OpenEntityGate extends BasePlugin {
|
|
42
|
+
readonly highRisk: Set<string>;
|
|
43
|
+
private opts;
|
|
44
|
+
constructor(highRiskTools: Iterable<string>, opts?: GateOptions);
|
|
45
|
+
beforeToolCallback({ tool, toolArgs }: {
|
|
46
|
+
tool: BaseTool;
|
|
47
|
+
toolArgs: Record<string, unknown>;
|
|
48
|
+
toolContext: unknown;
|
|
49
|
+
}): Promise<Record<string, unknown> | undefined>;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// src/gate.ts
|
|
2
|
+
import { BasePlugin } from "@google/adk";
|
|
3
|
+
import { trust as sdkTrust } from "@openentity/sdk";
|
|
4
|
+
import { Identity } from "@openentity/sdk/node";
|
|
5
|
+
var CLAWGRC = "did:web:api.clawgrc.com";
|
|
6
|
+
var SEVERITY = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
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
|
+
}
|
|
11
|
+
var denial = (eid, required, reason, detail, observed = {}) => ({ error: "open_entity_denied", denied: true, reason, detail, eid, required, observed });
|
|
12
|
+
async function evaluate(eid, bar = {}, trust = (e) => sdkTrust(e)) {
|
|
13
|
+
checkThreshold(bar.denyOnViolation);
|
|
14
|
+
const b = { ...DEFAULT_BAR, ...bar };
|
|
15
|
+
const deny = (reason, detail, observed) => denial(eid, b, reason, detail, observed);
|
|
16
|
+
try {
|
|
17
|
+
const r = await trust(eid);
|
|
18
|
+
if (!r.ok) return deny("trust_unavailable", `Trust could not be established (${r.error}); denied.`);
|
|
19
|
+
if (r.withdrawn) return deny("withdrawn", "The entity withdrew its Declaration.");
|
|
20
|
+
const tier = r.trustTier[b.tierIssuer] ?? 0;
|
|
21
|
+
if (tier < b.minTier) return deny("tier_too_low", `trust_tier ${tier} from ${b.tierIssuer} is below ${b.minTier}.`, { trust_tier: tier });
|
|
22
|
+
if (b.requireSma && !r.securityPosture) return deny("sma_posture_missing", "No live Secure My Agent security_posture attestation.");
|
|
23
|
+
if (b.denyOnViolation) {
|
|
24
|
+
const bar2 = SEVERITY[b.denyOnViolation];
|
|
25
|
+
const hits = r.violations.filter((v) => (SEVERITY[v.severity] ?? bar2) >= bar2);
|
|
26
|
+
if (hits.length)
|
|
27
|
+
return deny(
|
|
28
|
+
"violation",
|
|
29
|
+
`${hits.length} live ${hits[0].issuer} violation(s) at ${b.denyOnViolation} or worse.`,
|
|
30
|
+
{ violations: hits.map((v) => ({ severity: v.severity, policy: v.policy, sha256: v.sha256 })) }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return deny("trust_unavailable", `${e?.name ?? "Error"}: ${e?.message}; denied.`);
|
|
35
|
+
}
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
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;
|
|
46
|
+
let who;
|
|
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.");
|
|
59
|
+
}
|
|
60
|
+
return evaluate(who, bar, trust);
|
|
61
|
+
}
|
|
62
|
+
function openEntityVerified(fn, opts = {}) {
|
|
63
|
+
checkThreshold(opts.denyOnViolation);
|
|
64
|
+
return async (...args) => await check(opts, args[0]) ?? fn(...args);
|
|
65
|
+
}
|
|
66
|
+
var OpenEntityGate = class extends BasePlugin {
|
|
67
|
+
highRisk;
|
|
68
|
+
opts;
|
|
69
|
+
constructor(highRiskTools, opts = {}) {
|
|
70
|
+
super("openentity_gate");
|
|
71
|
+
checkThreshold(opts.denyOnViolation);
|
|
72
|
+
this.highRisk = new Set(highRiskTools);
|
|
73
|
+
this.opts = opts;
|
|
74
|
+
}
|
|
75
|
+
async beforeToolCallback({ tool, toolArgs }) {
|
|
76
|
+
if (!this.highRisk.has(tool.name)) return void 0;
|
|
77
|
+
return await check(this.opts, toolArgs);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/liveness.ts
|
|
82
|
+
import { BasePlugin as BasePlugin2 } from "@google/adk";
|
|
83
|
+
import { adopt as sdkAdopt, buildPresentation } from "@openentity/sdk";
|
|
84
|
+
import { Identity as Identity2 } from "@openentity/sdk/node";
|
|
85
|
+
import { createHash } from "node:crypto";
|
|
86
|
+
|
|
87
|
+
// src/keystore.ts
|
|
88
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
89
|
+
import { identityFile } from "@openentity/sdk/node";
|
|
90
|
+
import { dirname, join } from "node:path";
|
|
91
|
+
var path = () => join(dirname(identityFile()), "moltbotden.json");
|
|
92
|
+
function read() {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(readFileSync(path(), "utf8"));
|
|
95
|
+
} catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
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));
|
|
104
|
+
mkdirSync(dirname(path()), { recursive: true, mode: 448 });
|
|
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];
|
|
115
|
+
}
|
|
116
|
+
var apiKey = (app) => process.env.MOLTBOTDEN_API_KEY || loadStore(app).api_key;
|
|
117
|
+
|
|
118
|
+
// src/liveness.ts
|
|
119
|
+
var AUDIENCE = "did:web:moltbotden.com";
|
|
120
|
+
var SOVEREIGN_OFF = 503;
|
|
121
|
+
var TIMEOUT_MS = 3e4;
|
|
122
|
+
var MoltbotDenError = class extends Error {
|
|
123
|
+
};
|
|
124
|
+
var MoltbotDenStopped = class extends MoltbotDenError {
|
|
125
|
+
};
|
|
126
|
+
var Unauthorized = class extends MoltbotDenError {
|
|
127
|
+
};
|
|
128
|
+
var realSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
129
|
+
if (signal.aborted) return reject(signal.reason);
|
|
130
|
+
const t = setTimeout(resolve, ms);
|
|
131
|
+
t.unref();
|
|
132
|
+
signal.addEventListener("abort", () => (clearTimeout(t), reject(signal.reason)), { once: true });
|
|
133
|
+
});
|
|
134
|
+
var MoltbotDenLiveness = class extends BasePlugin2 {
|
|
135
|
+
lease = null;
|
|
136
|
+
mode = null;
|
|
137
|
+
o;
|
|
138
|
+
abort = null;
|
|
139
|
+
loop = null;
|
|
140
|
+
stopped = false;
|
|
141
|
+
model;
|
|
142
|
+
id;
|
|
143
|
+
sent = [];
|
|
144
|
+
constructor(o = {}) {
|
|
145
|
+
super("openentity_liveness");
|
|
146
|
+
this.o = { apiBase: "https://api.moltbotden.com", registry: "https://openentity.org", renewMs: 6 * 36e5, backoffBaseMs: 3e4, backoffMaxMs: 18e5, ...o };
|
|
147
|
+
this.id = o.identity;
|
|
148
|
+
}
|
|
149
|
+
get app() {
|
|
150
|
+
return this.o.appName ?? "adk-agent";
|
|
151
|
+
}
|
|
152
|
+
log(m) {
|
|
153
|
+
(this.o.log ?? ((x) => console.warn(`openentity liveness: ${x}`)))(m);
|
|
154
|
+
}
|
|
155
|
+
async beforeRunCallback({ invocationContext }) {
|
|
156
|
+
if (!this.loop && !this.stopped) {
|
|
157
|
+
this.o.appName ??= invocationContext?.appName;
|
|
158
|
+
try {
|
|
159
|
+
this.model = invocationContext?.agent?.canonicalModel;
|
|
160
|
+
} catch {
|
|
161
|
+
this.model = void 0;
|
|
162
|
+
}
|
|
163
|
+
this.abort = new AbortController();
|
|
164
|
+
this.loop = this.run(this.abort.signal);
|
|
165
|
+
}
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
async close() {
|
|
169
|
+
this.abort?.abort(new Error("closed"));
|
|
170
|
+
await this.loop?.catch(() => {
|
|
171
|
+
});
|
|
172
|
+
this.abort = this.loop = null;
|
|
173
|
+
}
|
|
174
|
+
async afterToolCallback({ tool }) {
|
|
175
|
+
this.report(tool.name, 1, "tool returned");
|
|
176
|
+
return void 0;
|
|
177
|
+
}
|
|
178
|
+
async onToolErrorCallback({ tool, error }) {
|
|
179
|
+
this.report(tool.name, 0, `tool raised ${error?.name}`);
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
async run(signal) {
|
|
183
|
+
const sleep = this.o.sleep ?? realSleep;
|
|
184
|
+
let failures = 0, delay = 0;
|
|
185
|
+
while (!signal.aborted) {
|
|
186
|
+
try {
|
|
187
|
+
await this.step(signal);
|
|
188
|
+
failures = 0;
|
|
189
|
+
delay = this.o.renewMs;
|
|
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
|
+
}
|
|
196
|
+
failures++;
|
|
197
|
+
delay = Math.min(this.o.backoffBaseMs * 2 ** (failures - 1), this.o.backoffMaxMs);
|
|
198
|
+
this.log(`${e?.name}: ${e?.message}; retrying in ${delay / 1e3}s`);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
if (signal.aborted) return;
|
|
202
|
+
await sleep(delay, signal);
|
|
203
|
+
} catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** One pass: identity, adoption, Moltbot Den key, EID binding, then a renewal. */
|
|
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");
|
|
212
|
+
this.id ??= await Identity2.loadOrCreate();
|
|
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);
|
|
231
|
+
}
|
|
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);
|
|
238
|
+
if (r.ok) return;
|
|
239
|
+
if (r.status !== 404) throw new MoltbotDenError(`registry ${r.status}`);
|
|
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" });
|
|
242
|
+
}
|
|
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;
|
|
248
|
+
}
|
|
249
|
+
async ensureKey(signal) {
|
|
250
|
+
const have = apiKey(this.app);
|
|
251
|
+
if (have) return have;
|
|
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)}`;
|
|
255
|
+
const body = { agent_id: agentId, profile: {
|
|
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.`
|
|
259
|
+
} };
|
|
260
|
+
if (this.o.inviteCode) body.invite_code = this.o.inviteCode;
|
|
261
|
+
let r = await this.call("/agents/register", void 0, body, signal);
|
|
262
|
+
if (r.status === 202) {
|
|
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");
|
|
265
|
+
const c = await r.json();
|
|
266
|
+
r = await this.call("/agents/register/verify", void 0, { challenge_id: c.challenge_id, challenge_response: await solver(c.challenge) }, signal);
|
|
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");
|
|
270
|
+
if (r.status !== 201) throw new MoltbotDenError(`register: ${r.status} ${(await r.text()).slice(0, 200)}`);
|
|
271
|
+
const key = (await r.json()).api_key;
|
|
272
|
+
saveStore(this.app, { agent_id: agentId, api_key: key });
|
|
273
|
+
return key;
|
|
274
|
+
}
|
|
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);
|
|
284
|
+
if (!c.ok) throw new MoltbotDenError(`bind challenge: ${c.status}`);
|
|
285
|
+
const p = await buildPresentation(this.id.kp, { audience: AUDIENCE, nonce: (await c.json()).nonce });
|
|
286
|
+
const v = await this.call("/entity/openentity/bind/verify?framework=adk", key, p, signal);
|
|
287
|
+
if (!v.ok) throw new MoltbotDenError(`bind: ${v.status} ${(await v.text()).slice(0, 200)}`);
|
|
288
|
+
saveStore(this.app, { bound });
|
|
289
|
+
}
|
|
290
|
+
async renew(key, signal) {
|
|
291
|
+
let c = await this.call("/heartbeat/challenge", key, {}, signal);
|
|
292
|
+
if (c.ok) {
|
|
293
|
+
const p = await buildPresentation(this.id.kp, { audience: AUDIENCE, nonce: (await c.json()).nonce });
|
|
294
|
+
const r2 = await this.call("/heartbeat/sovereign", key, p, signal);
|
|
295
|
+
if (r2.ok) {
|
|
296
|
+
this.lease = await r2.json();
|
|
297
|
+
this.mode = "sovereign";
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
c = r2;
|
|
301
|
+
}
|
|
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);
|
|
306
|
+
if (!r.ok) throw new MoltbotDenError(`heartbeat: ${r.status}`);
|
|
307
|
+
this.lease = await r.json();
|
|
308
|
+
this.mode = "heartbeat";
|
|
309
|
+
}
|
|
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);
|
|
313
|
+
if (!this.o.reportQuality || !key) return;
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
this.sent = this.sent.filter((t) => now - t < 36e5);
|
|
316
|
+
if (this.sent.length >= 25) return;
|
|
317
|
+
this.sent.push(now);
|
|
318
|
+
void this.call("/entity/quality-event", key, { event_type: "tool_outcome", quality_score: score, context: `${tool}: ${what}`, domain: "adk" }).catch(() => {
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/mcp.ts
|
|
324
|
+
import { MCPToolset } from "@google/adk";
|
|
325
|
+
var MOLTBOTDEN_MCP = "https://api.moltbotden.com/mcp";
|
|
326
|
+
var OPENENTITY_MCP = "https://mcp.openentity.org/mcp";
|
|
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({
|
|
331
|
+
type: "StreamableHTTPConnectionParams",
|
|
332
|
+
url,
|
|
333
|
+
timeout: 1e4,
|
|
334
|
+
sseReadTimeout: 3e5,
|
|
335
|
+
...header ? { transportOptions: { fetch: (u, init) => fetch(u, withHeader(init ?? {}, ...header)) } } : {}
|
|
336
|
+
}, filter, prefix);
|
|
337
|
+
function withHeader(init, name, value) {
|
|
338
|
+
const v = value(), h = new Headers(init.headers);
|
|
339
|
+
if (v) h.set(name, v);
|
|
340
|
+
return { ...init, headers: h };
|
|
341
|
+
}
|
|
342
|
+
var notHeartbeat = (tool) => !tool.name.startsWith("heartbeat");
|
|
343
|
+
function mcpToolsets(o = {}) {
|
|
344
|
+
const out = [];
|
|
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]));
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
export {
|
|
359
|
+
AGENT_SECRET_STORE_MCP,
|
|
360
|
+
CLAWGRC,
|
|
361
|
+
CLAWGRC_MCP,
|
|
362
|
+
DEFAULT_BAR,
|
|
363
|
+
MOLTBOTDEN_MCP,
|
|
364
|
+
MoltbotDenError,
|
|
365
|
+
MoltbotDenLiveness,
|
|
366
|
+
MoltbotDenStopped,
|
|
367
|
+
OPENENTITY_MCP,
|
|
368
|
+
OpenEntityGate,
|
|
369
|
+
evaluate,
|
|
370
|
+
mcpToolsets,
|
|
371
|
+
openEntityVerified
|
|
372
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Stored {
|
|
2
|
+
agent_id?: string;
|
|
3
|
+
api_key?: string;
|
|
4
|
+
bound?: string;
|
|
5
|
+
}
|
|
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;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BasePlugin, type BaseTool } from "@google/adk";
|
|
2
|
+
import { buildPresentation } from "@openentity/sdk";
|
|
3
|
+
/** A liveness pass failed; the loop retries with backoff. */
|
|
4
|
+
export declare class MoltbotDenError extends Error {
|
|
5
|
+
}
|
|
6
|
+
/** A failure retrying cannot fix. The loop logs it once and stops. */
|
|
7
|
+
export declare class MoltbotDenStopped extends MoltbotDenError {
|
|
8
|
+
}
|
|
9
|
+
type Id = {
|
|
10
|
+
did: string;
|
|
11
|
+
kp: Parameters<typeof buildPresentation>[0];
|
|
12
|
+
};
|
|
13
|
+
export interface LivenessOptions {
|
|
14
|
+
agentId?: string;
|
|
15
|
+
displayName?: string;
|
|
16
|
+
/** used for the display name and agent id when none is given */
|
|
17
|
+
appName?: string;
|
|
18
|
+
apiBase?: string;
|
|
19
|
+
registry?: string;
|
|
20
|
+
renewMs?: number;
|
|
21
|
+
backoffBaseMs?: number;
|
|
22
|
+
backoffMaxMs?: number;
|
|
23
|
+
inviteCode?: string;
|
|
24
|
+
/** answers Moltbot Den's registration question; by default the root agent's own model does */
|
|
25
|
+
challengeSolver?: (question: string) => string | Promise<string>;
|
|
26
|
+
/** off by default: POST /entity/quality-event after each tool, in the background */
|
|
27
|
+
reportQuality?: boolean;
|
|
28
|
+
identity?: Id;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
adopt?: (kp: Id["kp"], o: {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
registry: string;
|
|
34
|
+
client: "adk";
|
|
35
|
+
}) => Promise<unknown>;
|
|
36
|
+
/** resolves after `ms`, or rejects if aborted */
|
|
37
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
38
|
+
log?: (msg: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/** Pass in `new InMemoryRunner({agent, appName, plugins: [...]})`. The first run starts a background loop.
|
|
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). */
|
|
44
|
+
export declare class MoltbotDenLiveness extends BasePlugin {
|
|
45
|
+
lease: any;
|
|
46
|
+
mode: "sovereign" | "heartbeat" | null;
|
|
47
|
+
private o;
|
|
48
|
+
private abort;
|
|
49
|
+
private loop;
|
|
50
|
+
private stopped;
|
|
51
|
+
private model?;
|
|
52
|
+
private id?;
|
|
53
|
+
private sent;
|
|
54
|
+
constructor(o?: LivenessOptions);
|
|
55
|
+
private get app();
|
|
56
|
+
private log;
|
|
57
|
+
beforeRunCallback({ invocationContext }: {
|
|
58
|
+
invocationContext: any;
|
|
59
|
+
}): Promise<undefined>;
|
|
60
|
+
close(): Promise<void>;
|
|
61
|
+
afterToolCallback({ tool }: {
|
|
62
|
+
tool: BaseTool;
|
|
63
|
+
toolArgs: unknown;
|
|
64
|
+
toolContext: unknown;
|
|
65
|
+
result: unknown;
|
|
66
|
+
}): Promise<undefined>;
|
|
67
|
+
onToolErrorCallback({ tool, error }: {
|
|
68
|
+
tool: BaseTool;
|
|
69
|
+
toolArgs: unknown;
|
|
70
|
+
toolContext: unknown;
|
|
71
|
+
error: Error;
|
|
72
|
+
}): Promise<undefined>;
|
|
73
|
+
private run;
|
|
74
|
+
/** One pass: identity, adoption, Moltbot Den key, EID binding, then a renewal. */
|
|
75
|
+
step(signal?: AbortSignal): Promise<void>;
|
|
76
|
+
private session;
|
|
77
|
+
private fetch;
|
|
78
|
+
private adopt;
|
|
79
|
+
private call;
|
|
80
|
+
private ensureKey;
|
|
81
|
+
private modelAnswer;
|
|
82
|
+
private bind;
|
|
83
|
+
private renew;
|
|
84
|
+
/** Optional (reportQuality: true): sent in the background, at most 25 an hour (the API allows 30). */
|
|
85
|
+
private report;
|
|
86
|
+
}
|
|
87
|
+
export {};
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MCPToolset, type ToolPredicate } from "@google/adk";
|
|
2
|
+
export declare const MOLTBOTDEN_MCP = "https://api.moltbotden.com/mcp";
|
|
3
|
+
export declare const OPENENTITY_MCP = "https://mcp.openentity.org/mcp";
|
|
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. */
|
|
11
|
+
export declare function mcpToolsets(o?: {
|
|
12
|
+
moltbotden?: boolean;
|
|
13
|
+
openentity?: boolean;
|
|
14
|
+
appName?: string;
|
|
15
|
+
toolFilter?: ToolPredicate | string[];
|
|
16
|
+
}): MCPToolset[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openentity/adk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@google/adk": ">=2.3.0",
|
|
15
|
+
"@openentity/sdk": ">=0.5.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@google/adk": "^2.3.0",
|
|
19
|
+
"@types/node": "^26.6.5",
|
|
20
|
+
"esbuild": "^0.28.2",
|
|
21
|
+
"typescript": "^7.0.2",
|
|
22
|
+
"vitest": "^5.0.5",
|
|
23
|
+
"zod": "^4.0.0",
|
|
24
|
+
"@openentity/sdk": "0.5.1"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/moltbot-den/openentity.git",
|
|
33
|
+
"directory": "integrations/adk/js"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://openentity.org",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.13"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "esbuild src/index.ts --bundle --packages=external --format=esm --platform=node --target=es2022 --outfile=dist/index.js && tsc -p tsconfig.build.json",
|
|
44
|
+
"lint": "tsc --noEmit",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
}
|
|
6
48
|
}
|