feat: cross-gateway sessions_send and sessions_spawn via gateway.peers#43656
feat: cross-gateway sessions_send and sessions_spawn via gateway.peers#43656SPFAdvisors wants to merge 2 commits into
Conversation
Add cross-gateway messaging support so agents on different machines
(each running their own OpenClaw gateway) can communicate natively
without SSH script workarounds.
Changes:
1. gateway.peers config map (name -> {url, token})
- Zod schema, TypeScript type, schema label + help
- Peer tokens support SecretInput (env refs, secret stores)
2. sessions_send: gateway/gatewayUrl/gatewayToken params
- Named peer: gateway="imac" resolves from config
- Explicit: gatewayUrl + gatewayToken for ad-hoc targets
- Cross-gateway path: resolve label, send message, wait for
reply, fetch history — all via remote callGateway()
- Local path unchanged (zero breaking changes)
3. sessions_spawn: same gateway/gatewayUrl/gatewayToken params
- Forwards spawn as agent message to remote gateway
- Returns {status, runId, remote: true}
4. gateway-peer.ts: shared peer resolution helper
- Resolves named peers from config with token resolution
- Falls back to explicit gatewayUrl
- Clear error messages with available peer names
5. Tests: 9 new tests for peer resolution (all passing)
- Named peer with/without token
- Missing peer, no peers configured
- Explicit URL override, precedence rules
- Existing 17 sessions tests + 32 config tests still pass
Config example:
gateway:
peers:
imac:
url: wss://imac.local:18789
token: ${env:IMAC_GATEWAY_TOKEN}
studio:
url: wss://studio.local:18789
token: ${env:STUDIO_GATEWAY_TOKEN}
Usage:
sessions_send(sessionKey="agent:jarvis:main", message="hello", gateway="imac")
sessions_spawn(task="run tests", agentId="pepper", gateway="studio")
Closes openclaw#43605
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb76211b4c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ? Math.max(0, Math.floor(params.timeoutSeconds)) | ||
| : 30; | ||
| const timeoutMs = timeoutSeconds * 1000; | ||
| const gateway = resolveGatewayOptions(peerOpts); |
There was a problem hiding this comment.
Accept configured peer URLs in cross-gateway session routing
This path resolves gateway.peers and then immediately calls resolveGatewayOptions(peerOpts), but resolveGatewayOptions still enforces gatewayUrl to be loopback or gateway.remote.url via validateGatewayUrlOverrideForAgentTools (src/agents/tools/gateway.ts). In practice, a peer like wss://imac.local:18789 is rejected with gatewayUrl override rejected, so the new gateway parameter cannot reach most configured peers (same failure pattern also applies to sessions_spawn).
Useful? React with 👍 / 👎.
| params: { | ||
| message: task, | ||
| ...(sessionKey ? { sessionKey } : {}), |
There was a problem hiding this comment.
Include idempotencyKey when forwarding remote sessions_spawn
The remote sessions_spawn fast path forwards to the gateway agent method, but this request body omits idempotencyKey. The gateway protocol requires idempotencyKey for agent requests (AgentParamsSchema), so any sessions_spawn call with gateway/gatewayUrl will be rejected as invalid params before the remote agent run starts.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds cross-gateway routing to However, there is a critical runtime bug that makes the core feature non-functional:
Confidence Score: 1/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/tools/sessions-send-tool.ts
Line: 88
Comment:
**`resolveGatewayOptions` rejects all peer URLs — cross-gateway routing will always fail**
`resolveGatewayOptions` calls `validateGatewayUrlOverrideForAgentTools` internally, which only permits two categories of URL:
1. Loopback addresses (`127.0.0.1`, `localhost`, `[::1]`) on the configured local port
2. Exactly the URL in `gateway.remote.url`
Any peer URL like `wss://imac.local:18789` will cause `validateGatewayUrlOverrideForAgentTools` to throw:
```
"gatewayUrl override rejected. Allowed: ws(s) loopback on port X ..."
```
This error is **not caught** here — it will propagate as an unhandled exception and break the tool call. The same problem exists in `sessions-spawn-tool.ts` at the equivalent `resolveGatewayOptions(peerOpts)` call.
The PR description states "peer URLs are validated through existing `resolveGatewayOptions()`", but that function's allowlist was designed for a single remote gateway, not for arbitrary peer URLs. The cross-gateway feature will be entirely broken for any peer that is not also the configured `gateway.remote.url`.
The fix is to bypass `resolveGatewayOptions` for peer URLs that have already been validated by the config schema, and call `callGateway` directly with the resolved URL and token from `peerOpts`, similar to what the local path does but without the URL re-validation step.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/tools/gateway-peer.ts
Line: 48-51
Comment:
**Silent token resolution failure produces misleading auth errors**
When `resolveSecretInputString` fails (e.g., because `${env:IMAC_GATEWAY_TOKEN}` references an env var that isn't set), the error is silently swallowed. `resolvedToken` remains `undefined`, and the peer call proceeds without auth. The remote gateway will then reject the request with an authentication error that gives the user no indication that their token config is broken.
While the comment says "non-fatal if the peer gateway doesn't require auth", this hides a clear configuration error (a missing env var) behind a confusing downstream failure. At a minimum, the resolution failure should be surfaced as a warning in the thrown error message or re-thrown when the result is `undefined` and a token was explicitly configured.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/tools/sessions-spawn-tool.ts
Line: 127-146
Comment:
**Cross-gateway spawn silently drops all spawn-specific parameters**
When `peerOpts` is set, only `message`, `sessionKey`, `label`, `deliver`, `channel`, and `inputProvenance` are forwarded to the remote gateway. All of the following caller-provided parameters are silently ignored:
- `runtime` (subagent vs acp)
- `model`
- `thinking`
- `cwd`
- `runTimeoutSeconds` / `timeoutSeconds`
- `mode` (run vs session)
- `cleanup`
- `sandbox`
- `streamTo`
- `attachments` / `attachAs`
- `resumeSessionId`
A caller doing `sessions_spawn(task="...", agentId="pepper", gateway="studio", runtime="acp", model="gpt-4o")` will silently get whatever defaults the remote gateway applies, not the requested `runtime` or `model`. If these parameters are intentionally not supported for cross-gateway spawning, the tool should validate and reject their presence explicitly (like `UNSUPPORTED_SESSIONS_SPAWN_PARAM_KEYS` does for channel params) rather than silently dropping them.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/config/zod-schema.ts
Line: 813
Comment:
**No URL format validation on `gateway.peers[*].url`**
The `url` field is accepted as any non-empty string. A misconfigured value like `"http://imac.local:18789"` (wrong protocol) or `"imac.local:18789"` (missing scheme) will only fail at runtime when the connection is attempted, producing a confusing error rather than a clear config validation message.
Consider adding a `.url()` or a `refine` check for `ws://` / `wss://` prefix to catch misconfigurations at startup:
```suggestion
url: z.string().refine(
(v) => v.startsWith("ws://") || v.startsWith("wss://"),
{ message: "gateway peer url must start with ws:// or wss://" },
),
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: fb76211 |
| ? Math.max(0, Math.floor(params.timeoutSeconds)) | ||
| : 30; | ||
| const timeoutMs = timeoutSeconds * 1000; | ||
| const gateway = resolveGatewayOptions(peerOpts); |
There was a problem hiding this comment.
resolveGatewayOptions rejects all peer URLs — cross-gateway routing will always fail
resolveGatewayOptions calls validateGatewayUrlOverrideForAgentTools internally, which only permits two categories of URL:
- Loopback addresses (
127.0.0.1,localhost,[::1]) on the configured local port - Exactly the URL in
gateway.remote.url
Any peer URL like wss://imac.local:18789 will cause validateGatewayUrlOverrideForAgentTools to throw:
"gatewayUrl override rejected. Allowed: ws(s) loopback on port X ..."
This error is not caught here — it will propagate as an unhandled exception and break the tool call. The same problem exists in sessions-spawn-tool.ts at the equivalent resolveGatewayOptions(peerOpts) call.
The PR description states "peer URLs are validated through existing resolveGatewayOptions()", but that function's allowlist was designed for a single remote gateway, not for arbitrary peer URLs. The cross-gateway feature will be entirely broken for any peer that is not also the configured gateway.remote.url.
The fix is to bypass resolveGatewayOptions for peer URLs that have already been validated by the config schema, and call callGateway directly with the resolved URL and token from peerOpts, similar to what the local path does but without the URL re-validation step.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/sessions-send-tool.ts
Line: 88
Comment:
**`resolveGatewayOptions` rejects all peer URLs — cross-gateway routing will always fail**
`resolveGatewayOptions` calls `validateGatewayUrlOverrideForAgentTools` internally, which only permits two categories of URL:
1. Loopback addresses (`127.0.0.1`, `localhost`, `[::1]`) on the configured local port
2. Exactly the URL in `gateway.remote.url`
Any peer URL like `wss://imac.local:18789` will cause `validateGatewayUrlOverrideForAgentTools` to throw:
```
"gatewayUrl override rejected. Allowed: ws(s) loopback on port X ..."
```
This error is **not caught** here — it will propagate as an unhandled exception and break the tool call. The same problem exists in `sessions-spawn-tool.ts` at the equivalent `resolveGatewayOptions(peerOpts)` call.
The PR description states "peer URLs are validated through existing `resolveGatewayOptions()`", but that function's allowlist was designed for a single remote gateway, not for arbitrary peer URLs. The cross-gateway feature will be entirely broken for any peer that is not also the configured `gateway.remote.url`.
The fix is to bypass `resolveGatewayOptions` for peer URLs that have already been validated by the config schema, and call `callGateway` directly with the resolved URL and token from `peerOpts`, similar to what the local path does but without the URL re-validation step.
How can I resolve this? If you propose a fix, please make it concise.| } catch { | ||
| // Fall through — token resolution failure is non-fatal if the peer | ||
| // gateway doesn't require auth. | ||
| } |
There was a problem hiding this comment.
Silent token resolution failure produces misleading auth errors
When resolveSecretInputString fails (e.g., because ${env:IMAC_GATEWAY_TOKEN} references an env var that isn't set), the error is silently swallowed. resolvedToken remains undefined, and the peer call proceeds without auth. The remote gateway will then reject the request with an authentication error that gives the user no indication that their token config is broken.
While the comment says "non-fatal if the peer gateway doesn't require auth", this hides a clear configuration error (a missing env var) behind a confusing downstream failure. At a minimum, the resolution failure should be surfaced as a warning in the thrown error message or re-thrown when the result is undefined and a token was explicitly configured.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/gateway-peer.ts
Line: 48-51
Comment:
**Silent token resolution failure produces misleading auth errors**
When `resolveSecretInputString` fails (e.g., because `${env:IMAC_GATEWAY_TOKEN}` references an env var that isn't set), the error is silently swallowed. `resolvedToken` remains `undefined`, and the peer call proceeds without auth. The remote gateway will then reject the request with an authentication error that gives the user no indication that their token config is broken.
While the comment says "non-fatal if the peer gateway doesn't require auth", this hides a clear configuration error (a missing env var) behind a confusing downstream failure. At a minimum, the resolution failure should be surfaced as a warning in the thrown error message or re-thrown when the result is `undefined` and a token was explicitly configured.
How can I resolve this? If you propose a fix, please make it concise.| try { | ||
| const response = await callGateway<{ runId?: string }>({ | ||
| url: gateway.url, | ||
| token: gateway.token, | ||
| method: "agent", | ||
| params: { | ||
| message: task, | ||
| ...(sessionKey ? { sessionKey } : {}), | ||
| ...(label ? { label } : {}), | ||
| deliver: false, | ||
| channel: "internal", | ||
| inputProvenance: { | ||
| kind: "inter_session", | ||
| sourceSessionKey: opts?.agentSessionKey, | ||
| sourceChannel: opts?.agentChannel, | ||
| sourceTool: "sessions_spawn", | ||
| }, | ||
| }, | ||
| timeoutMs: 15_000, | ||
| }); |
There was a problem hiding this comment.
Cross-gateway spawn silently drops all spawn-specific parameters
When peerOpts is set, only message, sessionKey, label, deliver, channel, and inputProvenance are forwarded to the remote gateway. All of the following caller-provided parameters are silently ignored:
runtime(subagent vs acp)modelthinkingcwdrunTimeoutSeconds/timeoutSecondsmode(run vs session)cleanupsandboxstreamToattachments/attachAsresumeSessionId
A caller doing sessions_spawn(task="...", agentId="pepper", gateway="studio", runtime="acp", model="gpt-4o") will silently get whatever defaults the remote gateway applies, not the requested runtime or model. If these parameters are intentionally not supported for cross-gateway spawning, the tool should validate and reject their presence explicitly (like UNSUPPORTED_SESSIONS_SPAWN_PARAM_KEYS does for channel params) rather than silently dropping them.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/sessions-spawn-tool.ts
Line: 127-146
Comment:
**Cross-gateway spawn silently drops all spawn-specific parameters**
When `peerOpts` is set, only `message`, `sessionKey`, `label`, `deliver`, `channel`, and `inputProvenance` are forwarded to the remote gateway. All of the following caller-provided parameters are silently ignored:
- `runtime` (subagent vs acp)
- `model`
- `thinking`
- `cwd`
- `runTimeoutSeconds` / `timeoutSeconds`
- `mode` (run vs session)
- `cleanup`
- `sandbox`
- `streamTo`
- `attachments` / `attachAs`
- `resumeSessionId`
A caller doing `sessions_spawn(task="...", agentId="pepper", gateway="studio", runtime="acp", model="gpt-4o")` will silently get whatever defaults the remote gateway applies, not the requested `runtime` or `model`. If these parameters are intentionally not supported for cross-gateway spawning, the tool should validate and reject their presence explicitly (like `UNSUPPORTED_SESSIONS_SPAWN_PARAM_KEYS` does for channel params) rather than silently dropping them.
How can I resolve this? If you propose a fix, please make it concise.| z.string().min(1), | ||
| z | ||
| .object({ | ||
| url: z.string(), |
There was a problem hiding this comment.
No URL format validation on gateway.peers[*].url
The url field is accepted as any non-empty string. A misconfigured value like "http://imac.local:18789" (wrong protocol) or "imac.local:18789" (missing scheme) will only fail at runtime when the connection is attempted, producing a confusing error rather than a clear config validation message.
Consider adding a .url() or a refine check for ws:// / wss:// prefix to catch misconfigurations at startup:
| url: z.string(), | |
| url: z.string().refine( | |
| (v) => v.startsWith("ws://") || v.startsWith("wss://"), | |
| { message: "gateway peer url must start with ws:// or wss://" }, | |
| ), |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/zod-schema.ts
Line: 813
Comment:
**No URL format validation on `gateway.peers[*].url`**
The `url` field is accepted as any non-empty string. A misconfigured value like `"http://imac.local:18789"` (wrong protocol) or `"imac.local:18789"` (missing scheme) will only fail at runtime when the connection is attempted, producing a confusing error rather than a clear config validation message.
Consider adding a `.url()` or a `refine` check for `ws://` / `wss://` prefix to catch misconfigurations at startup:
```suggestion
url: z.string().refine(
(v) => v.startsWith("ws://") || v.startsWith("wss://"),
{ message: "gateway peer url must start with ws:// or wss://" },
),
```
How can I resolve this? If you propose a fix, please make it concise.1. Bypass resolveGatewayOptions for peer URLs — it only allows loopback/remote URLs, rejecting valid peer addresses like wss://imac.local:18789. Both sessions-send-tool and sessions-spawn-tool now pass peer URL/token directly to callGateway. 2. Throw on token resolution failure in gateway-peer.ts — previously silently swallowed, which would cause auth failures on the remote gateway with no indication of misconfiguration. 3. Reject unsupported spawn parameters for cross-gateway spawns — runtime, model, thinking, cwd, timeouts, mode, cleanup, sandbox, streamTo, thread, attachments, resumeSessionId are local-only and were silently dropped. Now returns explicit error listing which params are unsupported. 4. Add ws:///wss:// URL format validation on gateway.peers URL in zod-schema via .refine() check. Addresses greptile review on PR openclaw#43656.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85c5b02419
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| return { | ||
| gatewayUrl: peer.url.trim(), | ||
| gatewayToken: gatewayToken || resolvedToken, |
There was a problem hiding this comment.
Reject tokenless peers before routing to overridden gateway
This helper returns peer overrides even when no token is available (gatewayToken resolves to undefined), and both new cross-gateway paths pass that straight into callGateway. callGateway treats any explicit url as an override and requires explicit credentials (ensureExplicitGatewayAuth), so peer entries without a token will fail with "gateway url override requires explicit credentials" before the remote request is attempted. That makes the new optional gateway.peers.<name>.token behavior non-functional for tokenless peers.
Useful? React with 👍 / 👎.
| const sessionKeyParam = readStringParam(params, "sessionKey"); | ||
| const labelParam = readStringParam(params, "label")?.trim() || undefined; | ||
| const labelAgentIdParam = readStringParam(params, "agentId")?.trim() || undefined; | ||
| if (!sessionKeyParam && !labelParam) { |
There was a problem hiding this comment.
Preserve sessionKey/label exclusivity in remote sessions_send
The remote branch only checks for the "neither provided" case, so when both sessionKey and label are sent it silently prefers sessionKey and skips label resolution. The local branch still rejects this combination, so cross-gateway behavior becomes inconsistent and can route messages to an unintended session instead of surfacing an input error.
Useful? React with 👍 / 👎.
|
This pull request has been automatically marked as stale due to inactivity. |
|
ClawSweeper review: did not complete due to Codex infrastructure failure. Reviewed July 8, 2026, 11:23 AM ET / 15:23 UTC. Summary PR surface: Source +397, Tests +97. Total +494 across 8 files. Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path. Review metrics: none identified. Stored data model Merge readiness This is a ClawSweeper/Codex infrastructure failure, not a PR readiness or patch-quality verdict. Risk before merge
Maintainer options:
Next step before merge
Review detailsBest possible solution: Retry the Codex review after fixing the execution failure. Do we have a high-confidence way to reproduce the issue? Unclear. The review failed before ClawSweeper could establish a reproduction path. Is this the best way to solve the issue? Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction. AGENTS.md: unclear because the file could not be read completely. Codex review notes: model internal, reasoning high; reviewed against fc05a8103b72. Label changesLabel changes:
Evidence reviewedPR surface: Source +397, Tests +97. Total +494 across 8 files. View PR surface stats
What I checked:
Likely related people:
How this review workflow works
|
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
|
This pull request has been automatically marked as stale due to inactivity. |
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing this implementation while keeping #43605 open as the canonical issue. The March 13 head is conflicting, has no current multi-gateway proof, and still omits the required spawn idempotency key. The raw Thanks @SPFAdvisors for exploring the feature. |
Problem
sessions_sendandsessions_spawnonly work within a single gateway. In multi-machine setups (agents split across MacBook, iMac, Mac Studio — each running their own OpenClaw gateway), there's no native way for agents to communicate cross-gateway. The current workaround is SSH scripts (msg-jarvis), which are fragile and bypass all gateway auth/routing.Solution
Add cross-gateway routing to session tools via three complementary mechanisms:
1.
gateway.peersconfig map (new)Named peer gateways so agents can target remote gateways by name instead of raw URLs:
Peer tokens support SecretInput (env refs, secret stores, plaintext).
2.
sessions_send:gateway/gatewayUrl/gatewayTokenparamsWhen cross-gateway params are present:
agentmethodremote: trueflag3.
sessions_spawn: same paramsForwards spawn as an agent message to the remote gateway.
Shared helper:
gateway-peer.tsResolves cross-gateway targeting from tool params:
gateway: "peerName"→ looks upgateway.peers[peerName]gatewayUrl+ optionalgatewayToken→ explicit URL overridePeer names take precedence when both
gatewayandgatewayUrlare provided.Design notes
message,cron,gateway,nodes,canvastools already acceptgatewayUrl/gatewayToken; this extends the pattern to session toolsresolveGatewayOptions()which enforces URL format and token resolution rulesChanges
config/zod-schema.tsgateway.peersrecord schemaconfig/types.gateway.tsGatewayPeerConfigtypeconfig/schema.labels.tsgateway.peersconfig/schema.help.tsgateway.peersagents/tools/gateway-peer.tsagents/tools/sessions-send-tool.tsagents/tools/sessions-spawn-tool.tsagents/tools/gateway-peer.test.tsTests
Closes #43605