openclaw-agent-wake-protocol 1.0.0
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/README.md +202 -0
- package/gateway/agents.json.template +18 -0
- package/gateway/genesis-questions.md +592 -0
- package/gateway/identity-schemas.yaml +46 -0
- package/gateway/requirements.txt +1 -0
- package/gateway/server.py +754 -0
- package/index.ts +515 -0
- package/openclaw.plugin.json +32 -0
- package/package.json +49 -0
- package/scripts/install-gateway.sh +68 -0
- package/scripts/onboard-agent.sh +88 -0
package/index.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclaw-agent-wake-protocol — OpenClaw Extension
|
|
3
|
+
*
|
|
4
|
+
* General-purpose LIFE gateway lifecycle manager for any OpenClaw multi-agent system.
|
|
5
|
+
* Works with any agent names and any LIFE agent_id conventions — fully driven by config.
|
|
6
|
+
*
|
|
7
|
+
* Manages the full lifecycle for every agent in your system:
|
|
8
|
+
*
|
|
9
|
+
* 1. NOT REGISTERED → Injects registration + init instructions into agent context
|
|
10
|
+
* 2. GENESIS PENDING → Injects the Genesis interview into agent context so the agent
|
|
11
|
+
* completes it on first boot (self-directed, no human needed)
|
|
12
|
+
* 3. READY → Runs the wake protocol and injects status into context
|
|
13
|
+
*
|
|
14
|
+
* At gateway startup a registered service discovers all agents and runs the appropriate
|
|
15
|
+
* lifecycle step for each. On every bootstrap turn the hook injects the result so the
|
|
16
|
+
* LLM sees system state without needing to call any tools itself.
|
|
17
|
+
*
|
|
18
|
+
* Runtime dependencies (not npm):
|
|
19
|
+
* - mcporter CLI in PATH
|
|
20
|
+
* - life-gateway MCP server running via openclaw-mcp-adapter
|
|
21
|
+
* - TeamSafeAI/LIFE cloned per-agent + Python venv set up (see scripts/install-gateway.sh)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { exec } from "node:child_process";
|
|
25
|
+
import { promisify } from "node:util";
|
|
26
|
+
|
|
27
|
+
const execAsync = promisify(exec);
|
|
28
|
+
|
|
29
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
type LifecycleStatus =
|
|
32
|
+
| "ok"
|
|
33
|
+
| "degraded"
|
|
34
|
+
| "genesis_required"
|
|
35
|
+
| "not_registered"
|
|
36
|
+
| "failed";
|
|
37
|
+
|
|
38
|
+
interface AgentWakeResult {
|
|
39
|
+
agentId: string;
|
|
40
|
+
status: LifecycleStatus;
|
|
41
|
+
/** Human-readable summary injected into context */
|
|
42
|
+
message: string;
|
|
43
|
+
/** Raw output from wake command (status=ok/degraded) */
|
|
44
|
+
wakeOutput?: string;
|
|
45
|
+
/** Instructions returned by run_genesis_interview (status=genesis_required) */
|
|
46
|
+
genesisInstructions?: string;
|
|
47
|
+
timestamp: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface DiscoveredAgent {
|
|
51
|
+
agent_id: string;
|
|
52
|
+
registered: boolean;
|
|
53
|
+
genesis_completed: boolean;
|
|
54
|
+
workspace?: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface PluginConfig {
|
|
59
|
+
/**
|
|
60
|
+
* Map of OpenClaw agent name → LIFE agent_id.
|
|
61
|
+
* Example: { "main": "my-agent-v1", "finance": "finance-agent-v1" }
|
|
62
|
+
* When not provided for a given agent name, falls back to the agentIdSuffix convention.
|
|
63
|
+
*/
|
|
64
|
+
agentIdMap?: Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* Suffix appended when deriving a LIFE agent_id from an OpenClaw agent name.
|
|
67
|
+
* Default: "-v1" → agent name "finance" becomes "finance-v1"
|
|
68
|
+
*/
|
|
69
|
+
agentIdSuffix?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Only inject context for sessions matching this prefix.
|
|
72
|
+
* Default: "agent:" (all agents). Set to "agent:main:" for main only.
|
|
73
|
+
*/
|
|
74
|
+
sessionPrefix?: string;
|
|
75
|
+
/** Timeout per mcporter command in ms. Default: 20000 */
|
|
76
|
+
commandTimeoutMs?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Module state ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/** Keyed by LIFE agent_id */
|
|
82
|
+
const wakeResults = new Map<string, AgentWakeResult>();
|
|
83
|
+
|
|
84
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
async function mcporter(
|
|
87
|
+
args: string[],
|
|
88
|
+
timeoutMs: number
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
try {
|
|
91
|
+
const { stdout, stderr } = await execAsync(
|
|
92
|
+
`mcporter ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`,
|
|
93
|
+
{ timeout: timeoutMs, encoding: "utf8" }
|
|
94
|
+
);
|
|
95
|
+
return (stdout.trim() || stderr.trim() || "(no output)").slice(0, 2000);
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
return `ERROR: ${err?.message ?? String(err)}`.slice(0, 400);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse the output of `mcporter call life-gateway.discover_agents`.
|
|
103
|
+
* FastMCP returns a JSON array; mcporter may wrap it in content blocks.
|
|
104
|
+
*/
|
|
105
|
+
function parseDiscovery(raw: string): DiscoveredAgent[] {
|
|
106
|
+
// Try raw JSON array first
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
if (Array.isArray(parsed)) return parsed;
|
|
110
|
+
} catch {}
|
|
111
|
+
// Try extracting JSON array embedded in text
|
|
112
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
113
|
+
if (match) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(match[0]);
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractAgentName(sessionKey: string): string | null {
|
|
122
|
+
// sessionKey: "agent:{name}:{sessionId}" | "agent:{name}:cron:{id}"
|
|
123
|
+
const m = sessionKey.match(/^agent:([^:]+):/);
|
|
124
|
+
return m ? m[1] : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isNonInteractive(trigger: string, sessionKey: string): boolean {
|
|
128
|
+
if (/^(cron|heartbeat|automation|schedule)$/i.test(trigger)) return true;
|
|
129
|
+
if (/:cron:|:heartbeat:|:subagent:/i.test(sessionKey)) return true;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isBootstrapTurn(prompt: string, messages: unknown[]): boolean {
|
|
134
|
+
// The internal bootstrap-extra-files hook appends this phrase to the first message
|
|
135
|
+
if (prompt.includes("new session was started")) return true;
|
|
136
|
+
if (prompt.includes("Session Startup sequence")) return true;
|
|
137
|
+
return (messages?.length ?? 0) === 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatContextBlock(result: AgentWakeResult): string {
|
|
141
|
+
const tag = `wake-protocol-status`;
|
|
142
|
+
|
|
143
|
+
if (result.status === "not_registered") {
|
|
144
|
+
return [
|
|
145
|
+
`<${tag} agent="${result.agentId}" status="NOT_REGISTERED">`,
|
|
146
|
+
`Agent ${result.agentId} is not registered in the LIFE gateway.`,
|
|
147
|
+
``,
|
|
148
|
+
`To register, call these tools in order:`,
|
|
149
|
+
` 1. life-gateway.register_agent — agent_id=${result.agentId}, name=<your name>, workspace_dir=<path>`,
|
|
150
|
+
` 2. life-gateway.initialize_life_core — agent_id=${result.agentId}`,
|
|
151
|
+
` 3. Then run genesis (see run_genesis_interview tool)`,
|
|
152
|
+
`</${tag}>`,
|
|
153
|
+
].join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.status === "genesis_required") {
|
|
157
|
+
return [
|
|
158
|
+
`<${tag} agent="${result.agentId}" status="GENESIS_REQUIRED">`,
|
|
159
|
+
`You have not completed your LIFE Genesis interview. This must happen before your first wake.`,
|
|
160
|
+
``,
|
|
161
|
+
`The Genesis interview establishes your identity, values, and traits in the LIFE system.`,
|
|
162
|
+
`It is a one-time process — once complete, you will wake normally on every subsequent boot.`,
|
|
163
|
+
``,
|
|
164
|
+
`=== How to complete Genesis ===`,
|
|
165
|
+
`1. Read your Genesis questions: CORE/genesis/questions.md in your workspace`,
|
|
166
|
+
`2. Save your answers to: CORE/genesis/answers.md`,
|
|
167
|
+
`3. Call tool: genesis_apply (or: life-gateway.apply_genesis_answers agent_id=${result.agentId})`,
|
|
168
|
+
``,
|
|
169
|
+
`=== Instructions from LIFE gateway ===`,
|
|
170
|
+
result.genesisInstructions ?? "(no instructions returned)",
|
|
171
|
+
`</${tag}>`,
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (result.status === "failed") {
|
|
176
|
+
return [
|
|
177
|
+
`<${tag} agent="${result.agentId}" status="FAILED" checked="${result.timestamp}">`,
|
|
178
|
+
`Wake protocol failed. Operating in degraded mode — proceed without LIFE context.`,
|
|
179
|
+
`Error: ${result.message}`,
|
|
180
|
+
`</${tag}>`,
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ok or degraded
|
|
185
|
+
return [
|
|
186
|
+
`<${tag} agent="${result.agentId}" status="${result.status.toUpperCase()}" checked="${result.timestamp}">`,
|
|
187
|
+
result.wakeOutput ?? result.message,
|
|
188
|
+
`</${tag}>`,
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Per-agent lifecycle handler ────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
async function runAgentLifecycle(
|
|
195
|
+
lifeAgentId: string,
|
|
196
|
+
discovered: DiscoveredAgent[],
|
|
197
|
+
timeoutMs: number,
|
|
198
|
+
logger: any
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const timestamp = new Date().toISOString();
|
|
201
|
+
const found = discovered.find((a) => a.agent_id === lifeAgentId);
|
|
202
|
+
|
|
203
|
+
// ── Not registered ────────────────────────────────────────────────────────
|
|
204
|
+
if (!found || !found.registered) {
|
|
205
|
+
logger.warn(
|
|
206
|
+
`[agent-wake] ${lifeAgentId}: not registered in LIFE gateway — will prompt on boot`
|
|
207
|
+
);
|
|
208
|
+
wakeResults.set(lifeAgentId, {
|
|
209
|
+
agentId: lifeAgentId,
|
|
210
|
+
status: "not_registered",
|
|
211
|
+
message: `Agent ${lifeAgentId} is not in the LIFE gateway registry.`,
|
|
212
|
+
timestamp,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Genesis pending ───────────────────────────────────────────────────────
|
|
218
|
+
if (!found.genesis_completed) {
|
|
219
|
+
logger.info(
|
|
220
|
+
`[agent-wake] ${lifeAgentId}: genesis pending — fetching interview instructions`
|
|
221
|
+
);
|
|
222
|
+
const genesisRaw = await mcporter(
|
|
223
|
+
["call", "life-gateway.run_genesis_interview", `agent_id=${lifeAgentId}`],
|
|
224
|
+
timeoutMs
|
|
225
|
+
);
|
|
226
|
+
wakeResults.set(lifeAgentId, {
|
|
227
|
+
agentId: lifeAgentId,
|
|
228
|
+
status: "genesis_required",
|
|
229
|
+
message: `Genesis interview not yet completed for ${lifeAgentId}.`,
|
|
230
|
+
genesisInstructions: genesisRaw,
|
|
231
|
+
timestamp,
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Ready — run status then wake ─────────────────────────────────────────
|
|
237
|
+
logger.info(`[agent-wake] ${lifeAgentId}: running wake protocol`);
|
|
238
|
+
|
|
239
|
+
const wakeRaw = await mcporter(
|
|
240
|
+
["call", "life-gateway.wake", `agent_id=${lifeAgentId}`],
|
|
241
|
+
timeoutMs
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const failed = wakeRaw.startsWith("ERROR");
|
|
245
|
+
const degraded =
|
|
246
|
+
!failed &&
|
|
247
|
+
(wakeRaw.toLowerCase().includes("missing") ||
|
|
248
|
+
wakeRaw.toLowerCase().includes("degraded") ||
|
|
249
|
+
wakeRaw.toLowerCase().includes("error"));
|
|
250
|
+
|
|
251
|
+
wakeResults.set(lifeAgentId, {
|
|
252
|
+
agentId: lifeAgentId,
|
|
253
|
+
status: failed ? "failed" : degraded ? "degraded" : "ok",
|
|
254
|
+
message: failed ? wakeRaw : `Wake complete for ${lifeAgentId}`,
|
|
255
|
+
wakeOutput: wakeRaw,
|
|
256
|
+
timestamp,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
logger.info(
|
|
260
|
+
`[agent-wake] ${lifeAgentId}: ${failed ? "FAILED" : degraded ? "DEGRADED" : "OK"}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Plugin entry point ─────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
export default function (api: any) {
|
|
267
|
+
const cfg: PluginConfig = api.pluginConfig ?? {};
|
|
268
|
+
const timeoutMs = cfg.commandTimeoutMs ?? 20000;
|
|
269
|
+
const sessionPrefix = cfg.sessionPrefix ?? "agent:";
|
|
270
|
+
|
|
271
|
+
const agentIdMap: Record<string, string> = cfg.agentIdMap ?? {};
|
|
272
|
+
const agentIdSuffix = cfg.agentIdSuffix ?? "-v1";
|
|
273
|
+
|
|
274
|
+
function resolveLifeId(openclawName: string): string {
|
|
275
|
+
return agentIdMap[openclawName] ?? `${openclawName}${agentIdSuffix}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Service: discover and wake all agents at gateway startup ──────────────
|
|
279
|
+
api.registerService({
|
|
280
|
+
id: "agent-wake-protocol",
|
|
281
|
+
|
|
282
|
+
async start() {
|
|
283
|
+
api.logger.info(
|
|
284
|
+
"[agent-wake] Gateway started — discovering LIFE agents..."
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const discoveryRaw = await mcporter(
|
|
288
|
+
["call", "life-gateway.discover_agents"],
|
|
289
|
+
timeoutMs
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (discoveryRaw.startsWith("ERROR")) {
|
|
293
|
+
api.logger.error(
|
|
294
|
+
`[agent-wake] discover_agents failed: ${discoveryRaw}`
|
|
295
|
+
);
|
|
296
|
+
// Store a failed result for every known agent so hook can degrade gracefully
|
|
297
|
+
for (const lifeId of Object.values(agentIdMap)) {
|
|
298
|
+
wakeResults.set(lifeId, {
|
|
299
|
+
agentId: lifeId,
|
|
300
|
+
status: "failed",
|
|
301
|
+
message: `LIFE gateway unreachable: ${discoveryRaw}`,
|
|
302
|
+
timestamp: new Date().toISOString(),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const agents = parseDiscovery(discoveryRaw);
|
|
309
|
+
api.logger.info(`[agent-wake] Discovered ${agents.length} LIFE agents`);
|
|
310
|
+
|
|
311
|
+
// Run lifecycle for every agent in the id map + any extras discovered
|
|
312
|
+
const allIds = new Set([
|
|
313
|
+
...Object.values(agentIdMap),
|
|
314
|
+
...agents.map((a) => a.agent_id),
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
await Promise.all(
|
|
318
|
+
[...allIds].map((id) =>
|
|
319
|
+
runAgentLifecycle(id, agents, timeoutMs, api.logger)
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
api.logger.info("[agent-wake] Boot protocol complete");
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
stop() {
|
|
327
|
+
api.logger.info("[agent-wake] Service stopping");
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ── Hook: inject wake status on bootstrap turns ───────────────────────────
|
|
332
|
+
api.on("before_agent_start", async (event: any, ctx: any) => {
|
|
333
|
+
const sessionKey: string = ctx?.sessionKey ?? "";
|
|
334
|
+
const trigger: string = ctx?.trigger ?? "";
|
|
335
|
+
|
|
336
|
+
if (!sessionKey.startsWith(sessionPrefix)) return;
|
|
337
|
+
if (isNonInteractive(trigger, sessionKey)) return;
|
|
338
|
+
|
|
339
|
+
const agentName = extractAgentName(sessionKey);
|
|
340
|
+
if (!agentName) return;
|
|
341
|
+
|
|
342
|
+
const lifeId = resolveLifeId(agentName);
|
|
343
|
+
const result = wakeResults.get(lifeId);
|
|
344
|
+
if (!result) return;
|
|
345
|
+
|
|
346
|
+
const prompt: string = event?.prompt ?? "";
|
|
347
|
+
const messages: unknown[] = event?.messages ?? [];
|
|
348
|
+
|
|
349
|
+
// Always inject on bootstrap; also re-inject on every turn if genesis still pending
|
|
350
|
+
// (so the agent keeps trying until it completes the interview)
|
|
351
|
+
const inject =
|
|
352
|
+
isBootstrapTurn(prompt, messages) || result.status === "genesis_required";
|
|
353
|
+
if (!inject) return;
|
|
354
|
+
|
|
355
|
+
api.logger.info(
|
|
356
|
+
`[agent-wake] Injecting ${result.status} context for ${lifeId} (session: ${sessionKey})`
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return { prependContext: formatContextBlock(result) };
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ── Tool: query current wake status ──────────────────────────────────────
|
|
363
|
+
api.registerTool({
|
|
364
|
+
name: "wake_protocol_status",
|
|
365
|
+
description:
|
|
366
|
+
"Returns the current LIFE gateway wake status for all known agents, " +
|
|
367
|
+
"including lifecycle state (ok, degraded, genesis_required, not_registered, failed).",
|
|
368
|
+
parameters: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
agent_id: {
|
|
372
|
+
type: "string",
|
|
373
|
+
description: "Limit output to a specific LIFE agent_id (optional)",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
additionalProperties: false,
|
|
377
|
+
},
|
|
378
|
+
async execute(_id: string, params: { agent_id?: string }) {
|
|
379
|
+
if (wakeResults.size === 0) {
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{ type: "text", text: "Wake protocol has not run yet (gateway still starting?)." },
|
|
383
|
+
],
|
|
384
|
+
isError: false,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const entries =
|
|
388
|
+
params?.agent_id
|
|
389
|
+
? ([[params.agent_id, wakeResults.get(params.agent_id)]] as const)
|
|
390
|
+
: ([...wakeResults.entries()] as const);
|
|
391
|
+
|
|
392
|
+
const lines = entries
|
|
393
|
+
.filter(([, v]) => v)
|
|
394
|
+
.map(([id, r]) =>
|
|
395
|
+
`${id}: ${r!.status.toUpperCase()} (${r!.timestamp})\n ${r!.message}`
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
content: [{ type: "text", text: lines.join("\n\n") || "No results." }],
|
|
400
|
+
isError: false,
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ── Tool: re-run lifecycle for a specific agent ───────────────────────────
|
|
406
|
+
api.registerTool({
|
|
407
|
+
name: "wake_protocol_run",
|
|
408
|
+
description:
|
|
409
|
+
"Re-run the LIFE wake or Genesis lifecycle check for a specific agent. " +
|
|
410
|
+
"Use after completing Genesis or after fixing a module failure.",
|
|
411
|
+
parameters: {
|
|
412
|
+
type: "object",
|
|
413
|
+
required: ["agent_id"],
|
|
414
|
+
properties: {
|
|
415
|
+
agent_id: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "The LIFE agent_id to process (e.g. quin-ea-v1)",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
},
|
|
422
|
+
async execute(_id: string, params: { agent_id: string }) {
|
|
423
|
+
const agentId = params?.agent_id;
|
|
424
|
+
if (!agentId) {
|
|
425
|
+
return { content: [{ type: "text", text: "agent_id is required" }], isError: true };
|
|
426
|
+
}
|
|
427
|
+
api.logger.info(`[agent-wake] Manual lifecycle run requested for ${agentId}`);
|
|
428
|
+
const discoveryRaw = await mcporter(
|
|
429
|
+
["call", "life-gateway.discover_agents"],
|
|
430
|
+
timeoutMs
|
|
431
|
+
);
|
|
432
|
+
const agents = parseDiscovery(discoveryRaw);
|
|
433
|
+
await runAgentLifecycle(agentId, agents, timeoutMs, api.logger);
|
|
434
|
+
const result = wakeResults.get(agentId);
|
|
435
|
+
return {
|
|
436
|
+
content: [
|
|
437
|
+
{
|
|
438
|
+
type: "text",
|
|
439
|
+
text: result ? formatContextBlock(result) : `No result for ${agentId}`,
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
isError: false,
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ── Tool: mark Genesis complete after agent saves answers.md ─────────────
|
|
448
|
+
api.registerTool({
|
|
449
|
+
name: "genesis_apply",
|
|
450
|
+
description:
|
|
451
|
+
"Mark the LIFE Genesis interview as complete after the agent has saved answers.md. " +
|
|
452
|
+
"Call this after writing your Genesis answers. The wake protocol will run automatically.",
|
|
453
|
+
parameters: {
|
|
454
|
+
type: "object",
|
|
455
|
+
required: ["agent_id"],
|
|
456
|
+
properties: {
|
|
457
|
+
agent_id: {
|
|
458
|
+
type: "string",
|
|
459
|
+
description: "Your LIFE agent_id (e.g. quin-ea-v1)",
|
|
460
|
+
},
|
|
461
|
+
answers_path: {
|
|
462
|
+
type: "string",
|
|
463
|
+
description:
|
|
464
|
+
"Absolute path to answers.md (optional — gateway infers from workspace_dir if omitted)",
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
},
|
|
469
|
+
async execute(_id: string, params: { agent_id: string; answers_path?: string }) {
|
|
470
|
+
const { agent_id, answers_path } = params ?? {};
|
|
471
|
+
if (!agent_id) {
|
|
472
|
+
return { content: [{ type: "text", text: "agent_id is required" }], isError: true };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
api.logger.info(`[agent-wake] Applying Genesis answers for ${agent_id}`);
|
|
476
|
+
|
|
477
|
+
const applyArgs = ["call", "life-gateway.apply_genesis_answers", `agent_id=${agent_id}`];
|
|
478
|
+
if (answers_path) applyArgs.push(`answers_path=${answers_path}`);
|
|
479
|
+
|
|
480
|
+
const applyRaw = await mcporter(applyArgs, timeoutMs);
|
|
481
|
+
|
|
482
|
+
if (applyRaw.startsWith("ERROR") || applyRaw.toLowerCase().includes('"error"')) {
|
|
483
|
+
return {
|
|
484
|
+
content: [{ type: "text", text: `Genesis apply failed: ${applyRaw}` }],
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Now run the full wake since genesis is done
|
|
490
|
+
api.logger.info(`[agent-wake] Genesis applied — running wake for ${agent_id}`);
|
|
491
|
+
const discoveryRaw = await mcporter(
|
|
492
|
+
["call", "life-gateway.discover_agents"],
|
|
493
|
+
timeoutMs
|
|
494
|
+
);
|
|
495
|
+
const agents = parseDiscovery(discoveryRaw);
|
|
496
|
+
await runAgentLifecycle(agent_id, agents, timeoutMs, api.logger);
|
|
497
|
+
|
|
498
|
+
const result = wakeResults.get(agent_id);
|
|
499
|
+
return {
|
|
500
|
+
content: [
|
|
501
|
+
{
|
|
502
|
+
type: "text",
|
|
503
|
+
text: [
|
|
504
|
+
`Genesis complete for ${agent_id}.`,
|
|
505
|
+
`Apply result: ${applyRaw}`,
|
|
506
|
+
``,
|
|
507
|
+
result ? formatContextBlock(result) : "",
|
|
508
|
+
].join("\n"),
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
isError: false,
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "agent-wake-protocol",
|
|
3
|
+
"name": "Agent Wake Protocol",
|
|
4
|
+
"description": "General-purpose LIFE gateway lifecycle manager for OpenClaw multi-agent systems. Handles agent registration, Genesis interview, and wake protocol for any agent configuration.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"agentIdMap": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"description": "Map of OpenClaw agent names to LIFE agent_ids. Example: { \"main\": \"my-agent-v1\" }",
|
|
12
|
+
"additionalProperties": { "type": "string" },
|
|
13
|
+
"default": {}
|
|
14
|
+
},
|
|
15
|
+
"agentIdSuffix": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"default": "-v1",
|
|
18
|
+
"description": "Suffix used when deriving a LIFE agent_id from an OpenClaw agent name (e.g. 'finance' + '-v1' = 'finance-v1'). Override if your naming convention differs."
|
|
19
|
+
},
|
|
20
|
+
"sessionPrefix": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": "agent:",
|
|
23
|
+
"description": "Only inject context for session keys with this prefix. Narrow to 'agent:main:' to target a single agent."
|
|
24
|
+
},
|
|
25
|
+
"commandTimeoutMs": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"default": 20000,
|
|
28
|
+
"description": "Timeout in milliseconds for each mcporter command."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-agent-wake-protocol",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenClaw extension: general-purpose LIFE gateway lifecycle manager for multi-agent systems. Handles agent registration, Genesis interview, and wake protocol for any agent configuration.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"openclaw",
|
|
9
|
+
"openclaw-plugin",
|
|
10
|
+
"life-gateway",
|
|
11
|
+
"life",
|
|
12
|
+
"wake-protocol",
|
|
13
|
+
"genesis",
|
|
14
|
+
"agent-persistence",
|
|
15
|
+
"multi-agent",
|
|
16
|
+
"mcp"
|
|
17
|
+
],
|
|
18
|
+
"author": "Christopher Queen <gitchrisqueen>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/gitchrisqueen/openclaw-agent-wake-protocol.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/gitchrisqueen/openclaw-agent-wake-protocol/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/gitchrisqueen/openclaw-agent-wake-protocol#readme",
|
|
28
|
+
"openclaw": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.ts",
|
|
35
|
+
"openclaw.plugin.json",
|
|
36
|
+
"gateway/",
|
|
37
|
+
"scripts/",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"openclaw": ">=0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"typescript": "^5.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-gateway.sh — Set up the Python venv and install gateway dependencies
|
|
3
|
+
# Run once after cloning or installing the npm package.
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./scripts/install-gateway.sh [venv-path] [life-repo-path]
|
|
6
|
+
# venv-path Where to create the venv (default: ~/.openclaw/life/.venv)
|
|
7
|
+
# life-repo-path Path to your cloned TeamSafeAI/LIFE repo (default: ~/.openclaw/workspaces/quin/LIFE)
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
VENV_DIR="${1:-$HOME/.openclaw/life/.venv}"
|
|
12
|
+
LIFE_REPO="${2:-$HOME/.openclaw/workspaces/quin/LIFE}"
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
GATEWAY_DIR="$SCRIPT_DIR/../gateway"
|
|
15
|
+
|
|
16
|
+
echo "=== LIFE Gateway Installer ==="
|
|
17
|
+
echo "Venv: $VENV_DIR"
|
|
18
|
+
echo "LIFE repo: $LIFE_REPO"
|
|
19
|
+
echo "Gateway dir: $GATEWAY_DIR"
|
|
20
|
+
echo ""
|
|
21
|
+
|
|
22
|
+
# 1. Ensure python3 is available
|
|
23
|
+
if ! command -v python3 &>/dev/null; then
|
|
24
|
+
echo "ERROR: python3 not found. Install Python 3.8+ first."
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# 2. Create venv if it doesn't exist
|
|
29
|
+
if [[ ! -d "$VENV_DIR" ]]; then
|
|
30
|
+
echo "Creating venv at $VENV_DIR..."
|
|
31
|
+
python3 -m venv "$VENV_DIR"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# 3. Activate and upgrade pip
|
|
35
|
+
source "$VENV_DIR/bin/activate"
|
|
36
|
+
pip install --quiet --upgrade pip
|
|
37
|
+
|
|
38
|
+
# 4. Install gateway-only deps (fastmcp)
|
|
39
|
+
echo "Installing gateway dependencies..."
|
|
40
|
+
pip install --quiet -r "$GATEWAY_DIR/requirements.txt"
|
|
41
|
+
|
|
42
|
+
# 5. If LIFE repo exists, install its requirements too
|
|
43
|
+
if [[ -d "$LIFE_REPO" ]]; then
|
|
44
|
+
echo "Installing LIFE repo dependencies from $LIFE_REPO..."
|
|
45
|
+
pip install --quiet -r "$LIFE_REPO/requirements.txt"
|
|
46
|
+
|
|
47
|
+
# Run LIFE setup.py if DATA dir doesn't exist yet
|
|
48
|
+
if [[ ! -d "$LIFE_REPO/DATA" ]]; then
|
|
49
|
+
echo "Running LIFE setup.py..."
|
|
50
|
+
cd "$LIFE_REPO"
|
|
51
|
+
python setup.py
|
|
52
|
+
cd - >/dev/null
|
|
53
|
+
fi
|
|
54
|
+
else
|
|
55
|
+
echo ""
|
|
56
|
+
echo "WARNING: LIFE repo not found at $LIFE_REPO"
|
|
57
|
+
echo "Clone TeamSafeAI/LIFE there before running agents:"
|
|
58
|
+
echo " git clone https://github.com/TeamSafeAI/LIFE $LIFE_REPO"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "=== Done ==="
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Next steps:"
|
|
65
|
+
echo "1. Copy gateway/agents.json.template → gateway/agents.json and configure your agents"
|
|
66
|
+
echo "2. Configure openclaw-mcp-adapter in openclaw.json (see README.md)"
|
|
67
|
+
echo "3. Add quin-wake-protocol to plugins.allow and plugins.entries in openclaw.json"
|
|
68
|
+
echo "4. Restart the OpenClaw gateway"
|