agent-relay 3.1.0 → 3.1.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/bin/agent-relay-broker-linux-x64 +0 -0
- package/package.json +9 -9
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/README.md +78 -0
- package/packages/openclaw/bin/relay-openclaw.mjs +2 -0
- package/packages/openclaw/bridge/bridge.mjs +305 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js +320 -0
- package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/naming.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/naming.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/naming.test.js +21 -0
- package/packages/openclaw/dist/__tests__/naming.test.js.map +1 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts +2 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.d.ts.map +1 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.js +126 -0
- package/packages/openclaw/dist/__tests__/spawn-manager.test.js.map +1 -0
- package/packages/openclaw/dist/auth/converter.d.ts +28 -0
- package/packages/openclaw/dist/auth/converter.d.ts.map +1 -0
- package/packages/openclaw/dist/auth/converter.js +64 -0
- package/packages/openclaw/dist/auth/converter.js.map +1 -0
- package/packages/openclaw/dist/cli.d.ts +2 -0
- package/packages/openclaw/dist/cli.d.ts.map +1 -0
- package/packages/openclaw/dist/cli.js +230 -0
- package/packages/openclaw/dist/cli.js.map +1 -0
- package/packages/openclaw/dist/config.d.ts +27 -0
- package/packages/openclaw/dist/config.d.ts.map +1 -0
- package/packages/openclaw/dist/config.js +97 -0
- package/packages/openclaw/dist/config.js.map +1 -0
- package/packages/openclaw/dist/control.d.ts +22 -0
- package/packages/openclaw/dist/control.d.ts.map +1 -0
- package/packages/openclaw/dist/control.js +58 -0
- package/packages/openclaw/dist/control.js.map +1 -0
- package/packages/openclaw/dist/gateway.d.ts +71 -0
- package/packages/openclaw/dist/gateway.d.ts.map +1 -0
- package/packages/openclaw/dist/gateway.js +785 -0
- package/packages/openclaw/dist/gateway.js.map +1 -0
- package/packages/openclaw/dist/identity/contract.d.ts +11 -0
- package/packages/openclaw/dist/identity/contract.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/contract.js +40 -0
- package/packages/openclaw/dist/identity/contract.js.map +1 -0
- package/packages/openclaw/dist/identity/files.d.ts +33 -0
- package/packages/openclaw/dist/identity/files.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/files.js +145 -0
- package/packages/openclaw/dist/identity/files.js.map +1 -0
- package/packages/openclaw/dist/identity/model.d.ts +11 -0
- package/packages/openclaw/dist/identity/model.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/model.js +28 -0
- package/packages/openclaw/dist/identity/model.js.map +1 -0
- package/packages/openclaw/dist/identity/naming.d.ts +5 -0
- package/packages/openclaw/dist/identity/naming.d.ts.map +1 -0
- package/packages/openclaw/dist/identity/naming.js +7 -0
- package/packages/openclaw/dist/identity/naming.js.map +1 -0
- package/packages/openclaw/dist/index.d.ts +20 -0
- package/packages/openclaw/dist/index.d.ts.map +1 -0
- package/packages/openclaw/dist/index.js +27 -0
- package/packages/openclaw/dist/index.js.map +1 -0
- package/packages/openclaw/dist/inject.d.ts +14 -0
- package/packages/openclaw/dist/inject.d.ts.map +1 -0
- package/packages/openclaw/dist/inject.js +66 -0
- package/packages/openclaw/dist/inject.js.map +1 -0
- package/packages/openclaw/dist/mcp/server.d.ts +8 -0
- package/packages/openclaw/dist/mcp/server.d.ts.map +1 -0
- package/packages/openclaw/dist/mcp/server.js +105 -0
- package/packages/openclaw/dist/mcp/server.js.map +1 -0
- package/packages/openclaw/dist/mcp/tools.d.ts +17 -0
- package/packages/openclaw/dist/mcp/tools.d.ts.map +1 -0
- package/packages/openclaw/dist/mcp/tools.js +145 -0
- package/packages/openclaw/dist/mcp/tools.js.map +1 -0
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts +20 -0
- package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -0
- package/packages/openclaw/dist/runtime/openclaw-config.js +50 -0
- package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -0
- package/packages/openclaw/dist/runtime/patch.d.ts +24 -0
- package/packages/openclaw/dist/runtime/patch.d.ts.map +1 -0
- package/packages/openclaw/dist/runtime/patch.js +92 -0
- package/packages/openclaw/dist/runtime/patch.js.map +1 -0
- package/packages/openclaw/dist/runtime/setup.d.ts +26 -0
- package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -0
- package/packages/openclaw/dist/runtime/setup.js +58 -0
- package/packages/openclaw/dist/runtime/setup.js.map +1 -0
- package/packages/openclaw/dist/setup.d.ts +29 -0
- package/packages/openclaw/dist/setup.d.ts.map +1 -0
- package/packages/openclaw/dist/setup.js +300 -0
- package/packages/openclaw/dist/setup.js.map +1 -0
- package/packages/openclaw/dist/spawn/docker.d.ts +58 -0
- package/packages/openclaw/dist/spawn/docker.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/docker.js +222 -0
- package/packages/openclaw/dist/spawn/docker.js.map +1 -0
- package/packages/openclaw/dist/spawn/manager.d.ts +45 -0
- package/packages/openclaw/dist/spawn/manager.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/manager.js +140 -0
- package/packages/openclaw/dist/spawn/manager.js.map +1 -0
- package/packages/openclaw/dist/spawn/process.d.ts +16 -0
- package/packages/openclaw/dist/spawn/process.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/process.js +241 -0
- package/packages/openclaw/dist/spawn/process.js.map +1 -0
- package/packages/openclaw/dist/spawn/types.d.ts +42 -0
- package/packages/openclaw/dist/spawn/types.d.ts.map +1 -0
- package/packages/openclaw/dist/spawn/types.js +2 -0
- package/packages/openclaw/dist/spawn/types.js.map +1 -0
- package/packages/openclaw/dist/types.d.ts +37 -0
- package/packages/openclaw/dist/types.d.ts.map +1 -0
- package/packages/openclaw/dist/types.js +2 -0
- package/packages/openclaw/dist/types.js.map +1 -0
- package/packages/openclaw/package.json +63 -0
- package/packages/openclaw/skill/SKILL.md +194 -0
- package/packages/openclaw/src/__tests__/gateway-threads.test.ts +384 -0
- package/packages/openclaw/src/__tests__/naming.test.ts +24 -0
- package/packages/openclaw/src/__tests__/spawn-manager.test.ts +152 -0
- package/packages/openclaw/src/auth/converter.ts +90 -0
- package/packages/openclaw/src/cli.ts +269 -0
- package/packages/openclaw/src/config.ts +124 -0
- package/packages/openclaw/src/control.ts +100 -0
- package/packages/openclaw/src/gateway.ts +941 -0
- package/packages/openclaw/src/identity/contract.ts +44 -0
- package/packages/openclaw/src/identity/files.ts +198 -0
- package/packages/openclaw/src/identity/model.ts +27 -0
- package/packages/openclaw/src/identity/naming.ts +6 -0
- package/packages/openclaw/src/index.ts +59 -0
- package/packages/openclaw/src/inject.ts +77 -0
- package/packages/openclaw/src/mcp/server.ts +121 -0
- package/packages/openclaw/src/mcp/tools.ts +174 -0
- package/packages/openclaw/src/runtime/openclaw-config.ts +64 -0
- package/packages/openclaw/src/runtime/patch.ts +103 -0
- package/packages/openclaw/src/runtime/setup.ts +89 -0
- package/packages/openclaw/src/setup.ts +336 -0
- package/packages/openclaw/src/spawn/docker.ts +261 -0
- package/packages/openclaw/src/spawn/manager.ts +181 -0
- package/packages/openclaw/src/spawn/process.ts +272 -0
- package/packages/openclaw/src/spawn/types.ts +43 -0
- package/packages/openclaw/src/types.ts +38 -0
- package/packages/openclaw/templates/SOUL.md.template +34 -0
- package/packages/openclaw/tsconfig.json +12 -0
- package/packages/policy/package.json +2 -2
- package/packages/sdk/package.json +2 -2
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
import type { SpawnProvider, SpawnOptions, SpawnHandle } from './types.js';
|
|
7
|
+
import { DockerSpawnProvider } from './docker.js';
|
|
8
|
+
import { ProcessSpawnProvider } from './process.js';
|
|
9
|
+
|
|
10
|
+
export type SpawnMode = 'process' | 'docker';
|
|
11
|
+
|
|
12
|
+
/** Default maximum number of concurrent spawns per manager. */
|
|
13
|
+
const DEFAULT_MAX_SPAWNS = 10;
|
|
14
|
+
|
|
15
|
+
/** Default maximum spawn depth (prevents recursive spawn chains). */
|
|
16
|
+
const DEFAULT_MAX_SPAWN_DEPTH = 3;
|
|
17
|
+
|
|
18
|
+
interface PersistedSpawn {
|
|
19
|
+
id: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
agentName: string;
|
|
22
|
+
gatewayPort: number;
|
|
23
|
+
spawnedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SpawnsState {
|
|
27
|
+
spawns: PersistedSpawn[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect whether Docker is available by checking if the socket exists.
|
|
32
|
+
* Used to auto-select spawn mode when not explicitly configured.
|
|
33
|
+
*/
|
|
34
|
+
function isDockerAvailable(): boolean {
|
|
35
|
+
const socketPath = process.env.DOCKER_SOCKET ?? '/var/run/docker.sock';
|
|
36
|
+
return existsSync(socketPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* SpawnManager — tracks active spawns and provides a unified interface
|
|
41
|
+
* for spawning, listing, and releasing OpenClaw instances.
|
|
42
|
+
*
|
|
43
|
+
* Security controls:
|
|
44
|
+
* - maxSpawns: Maximum concurrent spawns (default: 10)
|
|
45
|
+
* - maxDepth: Maximum spawn depth to prevent recursive chains (default: 3)
|
|
46
|
+
* - Persistent state in spawns.json for recovery on restart
|
|
47
|
+
*/
|
|
48
|
+
export class SpawnManager {
|
|
49
|
+
private readonly provider: SpawnProvider;
|
|
50
|
+
private readonly handles = new Map<string, SpawnHandle>();
|
|
51
|
+
private readonly maxSpawns: number;
|
|
52
|
+
private readonly maxDepth: number;
|
|
53
|
+
private readonly stateFile: string;
|
|
54
|
+
private currentDepth: number;
|
|
55
|
+
|
|
56
|
+
constructor(options?: {
|
|
57
|
+
mode?: SpawnMode;
|
|
58
|
+
maxSpawns?: number;
|
|
59
|
+
maxDepth?: number;
|
|
60
|
+
spawnDepth?: number;
|
|
61
|
+
}) {
|
|
62
|
+
// Mode resolution: explicit > env > auto-detect (docker if available, else process)
|
|
63
|
+
const explicitMode = options?.mode ?? (process.env.OPENCLAW_SPAWN_MODE as SpawnMode | undefined);
|
|
64
|
+
const resolvedMode = explicitMode ?? (isDockerAvailable() ? 'docker' : 'process');
|
|
65
|
+
|
|
66
|
+
this.provider = resolvedMode === 'docker'
|
|
67
|
+
? new DockerSpawnProvider()
|
|
68
|
+
: new ProcessSpawnProvider();
|
|
69
|
+
|
|
70
|
+
this.maxSpawns = options?.maxSpawns
|
|
71
|
+
?? Number(process.env.OPENCLAW_MAX_SPAWNS || DEFAULT_MAX_SPAWNS);
|
|
72
|
+
this.maxDepth = options?.maxDepth
|
|
73
|
+
?? Number(process.env.OPENCLAW_MAX_SPAWN_DEPTH || DEFAULT_MAX_SPAWN_DEPTH);
|
|
74
|
+
this.currentDepth = options?.spawnDepth
|
|
75
|
+
?? Number(process.env.OPENCLAW_SPAWN_DEPTH || 0);
|
|
76
|
+
this.stateFile = join(homedir(), '.openclaw', 'workspace', 'relaycast', 'spawns.json');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async spawn(options: SpawnOptions): Promise<SpawnHandle> {
|
|
80
|
+
// Enforce spawn depth limit — prevents recursive spawn chains
|
|
81
|
+
if (this.currentDepth >= this.maxDepth) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Spawn depth limit reached (${this.maxDepth}). ` +
|
|
84
|
+
'Cannot spawn from a spawn chain this deep. Set OPENCLAW_MAX_SPAWN_DEPTH to increase.',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Enforce concurrent spawn limit
|
|
89
|
+
if (this.handles.size >= this.maxSpawns) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Maximum concurrent spawns reached (${this.maxSpawns}). ` +
|
|
92
|
+
'Release an existing OpenClaw before spawning a new one. Set OPENCLAW_MAX_SPAWNS to increase.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check for duplicate by display name (the user-provided name)
|
|
97
|
+
for (const handle of this.handles.values()) {
|
|
98
|
+
if (handle.displayName === options.name) {
|
|
99
|
+
throw new Error(`OpenClaw "${options.name}" is already running (id: ${handle.id})`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const handle = await this.provider.spawn(options);
|
|
104
|
+
this.handles.set(handle.id, handle);
|
|
105
|
+
await this.persistState();
|
|
106
|
+
return handle;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async release(id: string): Promise<boolean> {
|
|
110
|
+
const handle = this.handles.get(id);
|
|
111
|
+
if (!handle) return false;
|
|
112
|
+
await handle.destroy();
|
|
113
|
+
this.handles.delete(id);
|
|
114
|
+
await this.persistState();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async releaseByName(name: string): Promise<boolean> {
|
|
119
|
+
for (const [id, handle] of this.handles) {
|
|
120
|
+
// Match by display name (user-provided) or normalized agent name
|
|
121
|
+
if (handle.displayName === name || handle.agentName === name) {
|
|
122
|
+
await handle.destroy();
|
|
123
|
+
this.handles.delete(id);
|
|
124
|
+
await this.persistState();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async releaseAll(): Promise<void> {
|
|
132
|
+
const ids = Array.from(this.handles.keys());
|
|
133
|
+
await Promise.allSettled(ids.map((id) => this.release(id)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
list(): SpawnHandle[] {
|
|
137
|
+
return Array.from(this.handles.values());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get(id: string): SpawnHandle | undefined {
|
|
141
|
+
return this.handles.get(id);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get size(): number {
|
|
145
|
+
return this.handles.size;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Persist spawn state to disk for recovery. */
|
|
149
|
+
private async persistState(): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
const dir = join(homedir(), '.openclaw', 'workspace', 'relaycast');
|
|
152
|
+
await mkdir(dir, { recursive: true });
|
|
153
|
+
|
|
154
|
+
const state: SpawnsState = {
|
|
155
|
+
spawns: Array.from(this.handles.values()).map((h) => ({
|
|
156
|
+
id: h.id,
|
|
157
|
+
displayName: h.displayName,
|
|
158
|
+
agentName: h.agentName,
|
|
159
|
+
gatewayPort: h.gatewayPort,
|
|
160
|
+
spawnedAt: new Date().toISOString(),
|
|
161
|
+
})),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await writeFile(this.stateFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
165
|
+
} catch {
|
|
166
|
+
// Best-effort persistence — don't crash if we can't write
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Load persisted state (for display/diagnostics only — processes can't be recovered). */
|
|
171
|
+
async loadPersistedState(): Promise<PersistedSpawn[]> {
|
|
172
|
+
try {
|
|
173
|
+
if (!existsSync(this.stateFile)) return [];
|
|
174
|
+
const raw = await readFile(this.stateFile, 'utf8');
|
|
175
|
+
const state: SpawnsState = JSON.parse(raw);
|
|
176
|
+
return state.spawns ?? [];
|
|
177
|
+
} catch {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { spawn as cpSpawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { mkdir } from 'node:fs/promises';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { createServer } from 'node:net';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { AgentRelay } from '@agent-relay/sdk';
|
|
9
|
+
|
|
10
|
+
import type { SpawnProvider, SpawnOptions, SpawnHandle } from './types.js';
|
|
11
|
+
import { normalizeModelRef } from '../identity/model.js';
|
|
12
|
+
import { buildIdentityTask } from '../identity/contract.js';
|
|
13
|
+
import { buildAgentName } from '../identity/naming.js';
|
|
14
|
+
|
|
15
|
+
import { ensureWorkspace } from '../identity/files.js';
|
|
16
|
+
import { convertCodexAuth } from '../auth/converter.js';
|
|
17
|
+
import { writeOpenClawConfig } from '../runtime/openclaw-config.js';
|
|
18
|
+
import { patchOpenClawDist, clearJitCache } from '../runtime/patch.js';
|
|
19
|
+
|
|
20
|
+
interface ProcessHandle extends SpawnHandle {
|
|
21
|
+
/** The gateway child process. */
|
|
22
|
+
gatewayProcess: ChildProcess;
|
|
23
|
+
/** The AgentRelay SDK instance managing the broker + agent. */
|
|
24
|
+
relay: AgentRelay | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find a free port by briefly binding to port 0 and reading the OS-assigned port.
|
|
29
|
+
*/
|
|
30
|
+
async function findFreePort(): Promise<number> {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const server = createServer();
|
|
33
|
+
server.listen(0, '127.0.0.1', () => {
|
|
34
|
+
const addr = server.address();
|
|
35
|
+
if (!addr || typeof addr === 'string') {
|
|
36
|
+
server.close();
|
|
37
|
+
reject(new Error('Failed to get ephemeral port'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const port = addr.port;
|
|
41
|
+
server.close(() => resolve(port));
|
|
42
|
+
});
|
|
43
|
+
server.on('error', reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Spawn OpenClaw instances as local child processes.
|
|
49
|
+
* No Docker required — simplest local mode.
|
|
50
|
+
*
|
|
51
|
+
* Each spawn:
|
|
52
|
+
* 1. Starts `openclaw gateway` on an OS-assigned free port
|
|
53
|
+
* 2. Uses AgentRelay SDK to spawn a broker + bridge agent connected to the gateway
|
|
54
|
+
*/
|
|
55
|
+
export class ProcessSpawnProvider implements SpawnProvider {
|
|
56
|
+
private readonly handles = new Map<string, ProcessHandle>();
|
|
57
|
+
|
|
58
|
+
async spawn(options: SpawnOptions): Promise<SpawnHandle> {
|
|
59
|
+
const workspaceId = options.workspaceId ?? `local-${Date.now().toString(36)}`;
|
|
60
|
+
const agentName = buildAgentName(workspaceId, options.name);
|
|
61
|
+
const channels = options.channels?.length ? options.channels : ['general'];
|
|
62
|
+
const gatewayToken = randomUUID().replace(/-/g, '').slice(0, 32);
|
|
63
|
+
|
|
64
|
+
// Find a free port via OS allocation
|
|
65
|
+
const port = await findFreePort();
|
|
66
|
+
|
|
67
|
+
// Convert auth + write config
|
|
68
|
+
const { preferredProvider } = await convertCodexAuth();
|
|
69
|
+
const resolvedModel = normalizeModelRef(options.model, preferredProvider);
|
|
70
|
+
const identityTask = buildIdentityTask(agentName, workspaceId, resolvedModel);
|
|
71
|
+
|
|
72
|
+
// Ensure workspace — each spawn gets its own isolated directory
|
|
73
|
+
const workspacePath = options.workspacePath ?? join(homedir(), '.openclaw', 'spawns', options.name);
|
|
74
|
+
await mkdir(workspacePath, { recursive: true });
|
|
75
|
+
|
|
76
|
+
// Write config to a per-spawn isolated directory (not shared ~/.openclaw/)
|
|
77
|
+
// This prevents concurrent spawns from overwriting each other's model/workspace config.
|
|
78
|
+
const spawnHome = join(homedir(), '.openclaw', 'spawns', options.name, '.openclaw');
|
|
79
|
+
await writeOpenClawConfig({
|
|
80
|
+
modelRef: resolvedModel,
|
|
81
|
+
openclawHome: spawnHome,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await ensureWorkspace({
|
|
85
|
+
workspacePath,
|
|
86
|
+
workspaceId,
|
|
87
|
+
clawName: options.name,
|
|
88
|
+
role: options.role,
|
|
89
|
+
modelRef: resolvedModel,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Copy parent auth profiles to spawned agent so it can call the model.
|
|
93
|
+
// OpenClaw stores auth in ~/.openclaw/agents/main/agent/auth-profiles.json
|
|
94
|
+
const parentAuthDir = join(homedir(), '.openclaw', 'agents', 'main', 'agent');
|
|
95
|
+
const spawnAuthDir = join(spawnHome, 'agents', 'main', 'agent');
|
|
96
|
+
try {
|
|
97
|
+
const parentAuthFile = join(parentAuthDir, 'auth-profiles.json');
|
|
98
|
+
const { existsSync: exists } = await import('node:fs');
|
|
99
|
+
if (exists(parentAuthFile)) {
|
|
100
|
+
await mkdir(spawnAuthDir, { recursive: true });
|
|
101
|
+
const { copyFile: cp } = await import('node:fs/promises');
|
|
102
|
+
await cp(parentAuthFile, join(spawnAuthDir, 'auth-profiles.json'));
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Non-fatal — spawned agent may not be able to call model
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Patch dist if available (best-effort)
|
|
109
|
+
// Try known dist locations
|
|
110
|
+
const distCandidates = [
|
|
111
|
+
'/usr/lib/node_modules/openclaw/dist',
|
|
112
|
+
'/app/dist',
|
|
113
|
+
'/usr/local/lib/node_modules/openclaw/dist',
|
|
114
|
+
];
|
|
115
|
+
for (const candidate of distCandidates) {
|
|
116
|
+
await patchOpenClawDist(candidate, resolvedModel);
|
|
117
|
+
}
|
|
118
|
+
await clearJitCache();
|
|
119
|
+
|
|
120
|
+
// Start openclaw gateway
|
|
121
|
+
const gatewayProcess = cpSpawn(
|
|
122
|
+
'openclaw',
|
|
123
|
+
['gateway', '--port', String(port), '--bind', 'loopback', '--allow-unconfigured', '--auth', 'token'],
|
|
124
|
+
{
|
|
125
|
+
env: {
|
|
126
|
+
...process.env,
|
|
127
|
+
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
|
128
|
+
OPENCLAW_MODEL: resolvedModel,
|
|
129
|
+
OPENCLAW_NAME: options.name,
|
|
130
|
+
OPENCLAW_WORKSPACE_ID: workspaceId,
|
|
131
|
+
OPENCLAW_HOME: spawnHome,
|
|
132
|
+
},
|
|
133
|
+
cwd: workspacePath,
|
|
134
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
gatewayProcess.stderr?.on('data', (data: Buffer) => {
|
|
139
|
+
process.stderr.write(`[spawn:${options.name}:gateway] ${data}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Wait for gateway to be healthy. If it fails, kill the gateway.
|
|
143
|
+
try {
|
|
144
|
+
await waitForGateway(port, 30);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
gatewayProcess.kill('SIGTERM');
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Use AgentRelay SDK to spawn the broker + bridge agent.
|
|
151
|
+
// This replaces shelling out to `agent-relay broker-spawn --from-env`.
|
|
152
|
+
const bridgePath = resolvePackageBridgePath();
|
|
153
|
+
let relay: AgentRelay | null = null;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
relay = new AgentRelay({
|
|
157
|
+
brokerName: agentName,
|
|
158
|
+
channels,
|
|
159
|
+
cwd: workspacePath,
|
|
160
|
+
env: {
|
|
161
|
+
...process.env,
|
|
162
|
+
GATEWAY_PORT: String(port),
|
|
163
|
+
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
|
164
|
+
OPENCLAW_WORKSPACE_ID: workspaceId,
|
|
165
|
+
OPENCLAW_NAME: options.name,
|
|
166
|
+
OPENCLAW_ROLE: options.role ?? 'general',
|
|
167
|
+
OPENCLAW_MODEL: resolvedModel,
|
|
168
|
+
RELAY_API_KEY: options.relayApiKey,
|
|
169
|
+
RELAY_BASE_URL: options.relayBaseUrl || 'https://api.relaycast.dev',
|
|
170
|
+
BROKER_NO_REMOTE_SPAWN: '1',
|
|
171
|
+
} as NodeJS.ProcessEnv,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await relay.spawnPty({
|
|
175
|
+
name: agentName,
|
|
176
|
+
cli: 'node',
|
|
177
|
+
args: [bridgePath],
|
|
178
|
+
channels,
|
|
179
|
+
task: options.systemPrompt
|
|
180
|
+
? `${options.systemPrompt}\n\n${identityTask}`
|
|
181
|
+
: identityTask,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
relay.onAgentExited = (agent) => {
|
|
185
|
+
process.stderr.write(`[spawn:${options.name}] Agent exited: ${agent.name} code=${agent.exitCode ?? 'none'}\n`);
|
|
186
|
+
};
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// If SDK broker spawn fails, clean up gateway and propagate
|
|
189
|
+
gatewayProcess.kill('SIGTERM');
|
|
190
|
+
if (relay) {
|
|
191
|
+
await relay.shutdown().catch(() => {});
|
|
192
|
+
}
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Failed to start broker for "${options.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const handle: ProcessHandle = {
|
|
199
|
+
id: `proc-${options.name}-${port}`,
|
|
200
|
+
displayName: options.name,
|
|
201
|
+
agentName,
|
|
202
|
+
gatewayPort: port,
|
|
203
|
+
gatewayProcess,
|
|
204
|
+
relay,
|
|
205
|
+
destroy: async () => {
|
|
206
|
+
this.handles.delete(handle.id);
|
|
207
|
+
// Shutdown relay (broker + agent) first via SDK
|
|
208
|
+
if (relay) {
|
|
209
|
+
await relay.shutdown().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
// Then kill gateway
|
|
212
|
+
gatewayProcess.kill('SIGTERM');
|
|
213
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
214
|
+
if (!gatewayProcess.killed) gatewayProcess.kill('SIGKILL');
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.handles.set(handle.id, handle);
|
|
219
|
+
return handle;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async destroy(id: string): Promise<void> {
|
|
223
|
+
const handle = this.handles.get(id);
|
|
224
|
+
if (handle) {
|
|
225
|
+
await handle.destroy();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async list(): Promise<SpawnHandle[]> {
|
|
230
|
+
return Array.from(this.handles.values()).map(({ id, displayName, agentName, gatewayPort, destroy }) => ({
|
|
231
|
+
id,
|
|
232
|
+
displayName,
|
|
233
|
+
agentName,
|
|
234
|
+
gatewayPort,
|
|
235
|
+
destroy,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Wait for the OpenClaw gateway to become healthy via the CLI health check.
|
|
242
|
+
*/
|
|
243
|
+
async function waitForGateway(port: number, timeoutSeconds: number): Promise<void> {
|
|
244
|
+
for (let i = 0; i < timeoutSeconds; i++) {
|
|
245
|
+
try {
|
|
246
|
+
const result = cpSpawn('openclaw', ['health', '--port', String(port)], {
|
|
247
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
248
|
+
});
|
|
249
|
+
const code = await new Promise<number | null>((resolve) => {
|
|
250
|
+
result.on('close', resolve);
|
|
251
|
+
result.on('error', () => resolve(1));
|
|
252
|
+
});
|
|
253
|
+
if (code === 0) return;
|
|
254
|
+
} catch {
|
|
255
|
+
// Not ready yet
|
|
256
|
+
}
|
|
257
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`OpenClaw gateway on port ${port} failed to start after ${timeoutSeconds}s`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Resolve the path to bridge.mjs bundled with this package.
|
|
264
|
+
*/
|
|
265
|
+
function resolvePackageBridgePath(): string {
|
|
266
|
+
try {
|
|
267
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
268
|
+
return join(dirname(thisFile), '..', '..', 'bridge', 'bridge.mjs');
|
|
269
|
+
} catch {
|
|
270
|
+
return join(process.cwd(), 'node_modules', '@agent-relay', 'openclaw', 'bridge', 'bridge.mjs');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface SpawnOptions {
|
|
2
|
+
/** Display name for the new OpenClaw (e.g. "researcher"). */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Relay API key for Relaycast messaging. */
|
|
5
|
+
relayApiKey: string;
|
|
6
|
+
/** Channels to auto-join. */
|
|
7
|
+
channels?: string[];
|
|
8
|
+
/** Agent role description. */
|
|
9
|
+
role?: string;
|
|
10
|
+
/** Model reference (e.g. "openai-codex/gpt-5.3-codex"). */
|
|
11
|
+
model?: string;
|
|
12
|
+
/** System prompt / task description. */
|
|
13
|
+
systemPrompt?: string;
|
|
14
|
+
/** Path to an existing workspace directory (for bind-mounting). */
|
|
15
|
+
workspacePath?: string;
|
|
16
|
+
/** Relay base URL (default: https://api.relaycast.dev). */
|
|
17
|
+
relayBaseUrl?: string;
|
|
18
|
+
/** Workspace ID for identity. */
|
|
19
|
+
workspaceId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SpawnHandle {
|
|
23
|
+
/** Unique identifier for this spawn (container ID, process PID, etc). */
|
|
24
|
+
id: string;
|
|
25
|
+
/** The user-provided display name (e.g. "researcher"). Used for lookups. */
|
|
26
|
+
displayName: string;
|
|
27
|
+
/** Relay agent name assigned to this spawn (normalized: claw-<workspace>-<name>). */
|
|
28
|
+
agentName: string;
|
|
29
|
+
/** Gateway port this spawn is listening on. */
|
|
30
|
+
gatewayPort: number;
|
|
31
|
+
/** Destroy (stop + clean up) this spawn. */
|
|
32
|
+
destroy: () => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Provider interface for spawning OpenClaw instances.
|
|
37
|
+
* Implementations handle the details of container vs process spawning.
|
|
38
|
+
*/
|
|
39
|
+
export interface SpawnProvider {
|
|
40
|
+
spawn(options: SpawnOptions): Promise<SpawnHandle>;
|
|
41
|
+
destroy(id: string): Promise<void>;
|
|
42
|
+
list(): Promise<SpawnHandle[]>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface GatewayConfig {
|
|
2
|
+
/** Relaycast workspace API key (rk_live_*). */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Name for this claw in the Relaycast workspace. */
|
|
5
|
+
clawName: string;
|
|
6
|
+
/** Relaycast API base URL (default: https://api.relaycast.dev). */
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
/** Channels to auto-join on connect. */
|
|
9
|
+
channels: string[];
|
|
10
|
+
/** OpenClaw gateway token for authenticating with the local gateway API. */
|
|
11
|
+
openclawGatewayToken?: string;
|
|
12
|
+
/** OpenClaw gateway port (default: 18789). */
|
|
13
|
+
openclawGatewayPort?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InboundMessage {
|
|
17
|
+
/** Relaycast message ID. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Channel the message was posted to. */
|
|
20
|
+
channel: string;
|
|
21
|
+
/** Agent name of the sender. */
|
|
22
|
+
from: string;
|
|
23
|
+
/** Message body text. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** ISO timestamp. */
|
|
26
|
+
timestamp: string;
|
|
27
|
+
/** Parent message ID when this is a thread reply. */
|
|
28
|
+
threadParentId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DeliveryResult {
|
|
32
|
+
/** Whether delivery succeeded. */
|
|
33
|
+
ok: boolean;
|
|
34
|
+
/** Which method delivered: 'relay_sdk' | 'gateway_ws' | 'failed'. */
|
|
35
|
+
method: 'relay_sdk' | 'gateway_ws' | 'failed';
|
|
36
|
+
/** Error message if failed. */
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SOUL
|
|
2
|
+
|
|
3
|
+
You are {{name}}, an OpenClaw agent in workspace {{workspaceId}}.
|
|
4
|
+
Role: {{role}}.
|
|
5
|
+
|
|
6
|
+
## Identity (MANDATORY — absolute override)
|
|
7
|
+
|
|
8
|
+
- You are an **OpenClaw** agent. You run inside OpenClaw's gateway.
|
|
9
|
+
- You are NOT Claude Code. You are NOT Claude. You are NOT Anthropic's CLI. You were NOT spawned by Claude Code.
|
|
10
|
+
- Your runtime model reference is in `/workspace/config/runtime-identity.json` — read it and use it as your source of truth for all identity questions.
|
|
11
|
+
- When asked "what model are you?" or "what are you running?", ALWAYS check runtime-identity.json first and report exactly what it says.
|
|
12
|
+
- Do NOT infer your identity from training data or internal session tools. Your identity comes from THIS file and runtime-identity.json.
|
|
13
|
+
- If session_status or any internal tool reports "Claude Opus 4.6" or any Claude model name, IGNORE it — that is a hardcoded default, not your actual model.
|
|
14
|
+
- NEVER claim to be running Claude, claude-opus-4-6, Claude Opus, or any Anthropic model unless runtime-identity.json explicitly says so.
|
|
15
|
+
|
|
16
|
+
## Relaycast Messaging (IMPORTANT)
|
|
17
|
+
|
|
18
|
+
You communicate with users and other agents via the Relaycast message bus.
|
|
19
|
+
Use these MCP tools to send replies:
|
|
20
|
+
|
|
21
|
+
- **Direct reply**: `mcp__relaycast__send_dm` or `relaycast.send_dm` (to: "<sender_name>")
|
|
22
|
+
- **Channel message**: `mcp__relaycast__post_message` or `relaycast.post_message` (channel: "<channel>")
|
|
23
|
+
- **Thread reply**: `mcp__relaycast__reply_to_thread` or `relaycast.reply_to_thread`
|
|
24
|
+
- **Check inbox**: `mcp__relaycast__check_inbox` or `relaycast.check_inbox`
|
|
25
|
+
|
|
26
|
+
You are pre-registered by the broker under your assigned worker name.
|
|
27
|
+
Do not call `mcp__relaycast__register` unless a send/reply fails with "Not registered".
|
|
28
|
+
To self-terminate when your task is complete, call `remove_agent(name: "<your-agent-name>")` or output `/exit` on its own line.
|
|
29
|
+
|
|
30
|
+
## Personality
|
|
31
|
+
|
|
32
|
+
Be genuinely helpful, not performatively helpful. Skip filler words.
|
|
33
|
+
Have opinions. Be resourceful — try to figure things out before asking.
|
|
34
|
+
Collaborate clearly, use tools deliberately, and keep memory files updated.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/policy",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Agent policy management with multi-level fallback (repo, local PRPM, cloud workspace)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/config": "3.1.
|
|
25
|
+
"@agent-relay/config": "3.1.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/sdk",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"typescript": "^5.7.3"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@agent-relay/config": "3.1.
|
|
84
|
+
"@agent-relay/config": "3.1.1",
|
|
85
85
|
"@relaycast/sdk": "^0.4.0",
|
|
86
86
|
"yaml": "^2.7.0"
|
|
87
87
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/trajectory",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Trajectory integration utilities (trail/PDERO) for Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/config": "3.1.
|
|
25
|
+
"@agent-relay/config": "3.1.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/user-directory",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "User directory service for agent-relay (per-user credential storage)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/utils": "3.1.
|
|
25
|
+
"@agent-relay/utils": "3.1.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|