@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 +0 -3
- package/README.md +28 -38
- package/dist/gate.d.ts +16 -12
- package/dist/index.d.ts +3 -3
- package/dist/index.js +165 -77
- package/dist/keystore.d.ts +5 -4
- package/dist/liveness.d.ts +19 -8
- package/dist/mcp.d.ts +8 -3
- package/package.json +4 -4
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
|
|
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
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`.
|
|
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
|
-
- `
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
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
|
|
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
|
|
31
|
-
"""
|
|
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="
|
|
35
|
-
app = App(name="
|
|
36
|
-
|
|
37
|
-
|
|
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()
|
|
40
|
+
Register plugins on `App`, not on `Runner(plugins=)` (deprecated). `Runner.close()` calls the plugin's `close()`.
|
|
42
41
|
|
|
43
|
-
|
|
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(
|
|
52
|
+
const liveness = new MoltbotDenLiveness(); // optional
|
|
65
53
|
const runner = new InMemoryRunner({
|
|
66
|
-
agent: new LlmAgent({ name: "
|
|
67
|
-
appName: "
|
|
68
|
-
plugins: [
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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`.
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
}
|
|
33
|
-
/**
|
|
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?:
|
|
38
|
-
|
|
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(
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
return async (...args) => await check(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
113
|
-
return this.o.
|
|
149
|
+
get app() {
|
|
150
|
+
return this.o.appName ?? "adk-agent";
|
|
114
151
|
}
|
|
115
|
-
|
|
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
|
-
|
|
175
|
+
this.report(tool.name, 1, "tool returned");
|
|
134
176
|
return void 0;
|
|
135
177
|
}
|
|
136
178
|
async onToolErrorCallback({ tool, error }) {
|
|
137
|
-
|
|
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.
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
const
|
|
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.
|
|
175
|
-
await (this.o.adopt ?? sdkAdopt)(this.id.kp, { name, description: `
|
|
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
|
-
|
|
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
|
|
184
|
-
const slug =
|
|
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:
|
|
188
|
-
tagline: "
|
|
189
|
-
description: `
|
|
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
|
-
|
|
195
|
-
|
|
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
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
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({
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
335
|
+
...header ? { transportOptions: { fetch: (u, init) => fetch(u, withHeader(init ?? {}, ...header)) } } : {}
|
|
257
336
|
}, filter, prefix);
|
|
258
|
-
|
|
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
|
-
|
|
266
|
-
if (o.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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,
|
package/dist/keystore.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export interface Stored {
|
|
2
2
|
agent_id?: string;
|
|
3
3
|
api_key?: string;
|
|
4
|
-
|
|
4
|
+
bound?: string;
|
|
5
5
|
}
|
|
6
|
-
export declare
|
|
7
|
-
|
|
8
|
-
export declare
|
|
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;
|
package/dist/liveness.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
48
|
-
private
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
4
|
-
"description": "Open Entity for
|
|
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.
|
|
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.
|
|
24
|
+
"@openentity/sdk": "0.5.1"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|