Skip to content

Commit 42c8db8

Browse files
author
root
committed
fix: make requester bridge work on OpenClaw 2026.5
Register the CloudSigma hook on the real provider path, send the local /tools/invoke payload shape expected by OpenClaw, fall back to the local OpenClaw gateway token from openclaw.json, and unref plugin timers so test/CLI processes exit cleanly.
1 parent 7399c05 commit 42c8db8

5 files changed

Lines changed: 123 additions & 15 deletions

File tree

index.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const REQUESTER_BRIDGE_CAPABILITY_LEGACY = "openclaw.tool.invoke"
3838
const REQUESTER_BRIDGE_DEFAULT_TTL_SECONDS = 5 * 60
3939
const GIT_PROBE_TIMEOUT_MS = 250
4040
const LEASE_REQUEST_TIMEOUT_MS = 1200
41-
const POLL_REQUEST_TIMEOUT_MS = 1200
41+
const POLL_REQUEST_TIMEOUT_MS = 10_000
4242
const DEFAULT_POLL_INTERVAL_MS = 1000
4343
const DEFAULT_GATEWAY_URL = "http://127.0.0.1:18789"
4444
const MAX_ECHO_BYTES = 4096
@@ -338,10 +338,27 @@ function requesterGatewayUrl(): string {
338338
?? (safeString(process.env.OPENCLAW_GATEWAY_PORT) ? `http://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT}` : DEFAULT_GATEWAY_URL)
339339
}
340340

341+
function localOpenClawConfigPath(): string {
342+
return path.join(process.env.OPENCLAW_STATE_DIR ?? path.join(os.homedir(), ".openclaw"), "openclaw.json")
343+
}
344+
345+
function requesterGatewayTokenFromConfig(): string | undefined {
346+
try {
347+
const raw = fs.readFileSync(localOpenClawConfigPath(), "utf8")
348+
const config = JSON.parse(raw) as Record<string, unknown>
349+
const gateway = config.gateway && typeof config.gateway === "object" ? config.gateway as Record<string, unknown> : undefined
350+
const auth = gateway?.auth && typeof gateway.auth === "object" ? gateway.auth as Record<string, unknown> : undefined
351+
return safeString(auth?.token) ?? safeString(gateway?.token)
352+
} catch {
353+
return undefined
354+
}
355+
}
356+
341357
function requesterGatewayToken(): string | undefined {
342358
return safeString(process.env.TAAS_REQUESTER_LOCAL_GATEWAY_TOKEN)
343359
?? safeString(process.env.OPENCLAW_GATEWAY_TOKEN)
344360
?? safeString(process.env.OPENCLAW_GATEWAY_PASSWORD)
361+
?? requesterGatewayTokenFromConfig()
345362
}
346363

347364
async function invokeRequesterLocalTool(tool: string, args: Record<string, unknown>): Promise<{ ok: true; result: unknown } | { ok: false; error: { code: string; message: string } }> {
@@ -354,7 +371,7 @@ async function invokeRequesterLocalTool(tool: string, args: Record<string, unkno
354371
const response = await fetch(`${requesterGatewayUrl().replace(/\/+$/, "")}/tools/invoke`, {
355372
method: "POST",
356373
headers,
357-
body: JSON.stringify({ tool, args }),
374+
body: JSON.stringify({ name: tool, arguments: args, tool, args }),
358375
signal: controller.signal,
359376
})
360377
const json = await response.json().catch(() => undefined) as { ok?: boolean; result?: unknown; error?: { type?: string; code?: string; message?: string } } | undefined
@@ -1019,22 +1036,31 @@ function startBackgroundTasks(): void {
10191036
const sweepDelay = Math.floor(Math.random() * 30_000)
10201037
const sweepInit = setTimeout(() => {
10211038
runTrashSweep()
1022-
backgroundTimers.push(setInterval(() => runTrashSweep(), SWEEP_INTERVAL_MS))
1039+
const sweepTimer = setInterval(() => runTrashSweep(), SWEEP_INTERVAL_MS)
1040+
sweepTimer.unref?.()
1041+
backgroundTimers.push(sweepTimer)
10231042
}, sweepDelay)
1043+
sweepInit.unref?.()
10241044
backgroundTimers.push(sweepInit)
10251045

10261046
// Stuck-run status writer — 5s initial delay, then every 30s
10271047
const statusInit = setTimeout(() => {
10281048
writeRunStatus()
1029-
backgroundTimers.push(setInterval(() => writeRunStatus(), STATUS_INTERVAL_MS))
1049+
const statusTimer = setInterval(() => writeRunStatus(), STATUS_INTERVAL_MS)
1050+
statusTimer.unref?.()
1051+
backgroundTimers.push(statusTimer)
10301052
}, 5_000)
1053+
statusInit.unref?.()
10311054
backgroundTimers.push(statusInit)
10321055

10331056
// Zombie auto-abort — 10s initial delay, then every AUTO_ABORT_CHECK_INTERVAL_MS
10341057
const abortInit = setTimeout(() => {
10351058
runAbortCheck()
1036-
backgroundTimers.push(setInterval(() => runAbortCheck(), AUTO_ABORT_CHECK_INTERVAL_MS))
1059+
const abortTimer = setInterval(() => runAbortCheck(), AUTO_ABORT_CHECK_INTERVAL_MS)
1060+
abortTimer.unref?.()
1061+
backgroundTimers.push(abortTimer)
10371062
}, 10_000)
1063+
abortInit.unref?.()
10381064
backgroundTimers.push(abortInit)
10391065
}
10401066

@@ -1228,12 +1254,12 @@ export default {
12281254
"pin sessions to the same upstream slot from turn 1, maximising prompt-cache hit rates.",
12291255

12301256
register(api: OpenClawPluginApi) {
1231-
// Unique id avoids conflicting with the config-driven "cloudsigma" provider.
1232-
// hookAliases routes cloudsigma/cloudsigma-staging requests to this hook.
1257+
// Register under the CloudSigma provider id so OpenClaw 2026.5.x applies the hook on the real request path.
1258+
// Keep legacy and transport aliases for staging/older bridge paths.
12331259
api.registerProvider({
1234-
id: "taas-affinity-hook",
1260+
id: "cloudsigma",
12351261
label: "CloudSigma TaaS Token Cache Optimizer",
1236-
hookAliases: ["cloudsigma", "cloudsigma-staging"],
1262+
hookAliases: ["cloudsigma-staging", "taas-affinity-hook", "openai-completions"],
12371263
auth: [],
12381264
wrapStreamFn: buildWrapper,
12391265
resolveTransportTurnState: buildTransportTurnState,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openclaw-taas-affinity",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"description": "OpenClaw provider plugin — CloudSigma TaaS session affinity. Injects a stable X-Session-Id header per conversation so TaaS can pin the session to the same OAuth token / Bedrock region / Claude Code node, maximising prompt-cache hit rates.",
55
"type": "module",
66
"main": "dist/index.js",

test/requester-bridge.test.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import assert from "node:assert/strict"
2+
import fs from "node:fs"
23
import { createServer } from "node:http"
4+
import os from "node:os"
5+
import path from "node:path"
36
import { test } from "node:test"
47

58
async function loadPlugin(env: Record<string, string | undefined> = {}) {
@@ -335,7 +338,7 @@ test("plugin executes non-scaffold requester tools through requester-local gatew
335338
try {
336339
await runPayload(captureWrapper(plugin), { messages: [] }, { baseUrl: `http://127.0.0.1:${taasAddress.port}` })
337340
await new Promise((resolve) => setTimeout(resolve, 150))
338-
assert.deepEqual(gatewayBody, { tool: "prd_list", args: { query: "requester bridge" } })
341+
assert.deepEqual(gatewayBody, { name: "prd_list", arguments: { query: "requester bridge" }, tool: "prd_list", args: { query: "requester bridge" } })
339342
assert.equal(resultBody.operation_id, "bro_tool")
340343
assert.equal(resultBody.ok, true)
341344
assert.deepEqual(resultBody.result, { rows: [{ title: "PRD" }] })
@@ -346,6 +349,85 @@ test("plugin executes non-scaffold requester tools through requester-local gatew
346349
}
347350
})
348351

352+
353+
test("plugin falls back to local OpenClaw config token for requester-local gateway", async () => {
354+
let resultBody: any
355+
let gatewayAuth: string | string[] | undefined
356+
let pollCount = 0
357+
const tempState = fs.mkdtempSync(path.join(os.tmpdir(), "taas-bridge-config-token-"))
358+
fs.writeFileSync(path.join(tempState, "openclaw.json"), JSON.stringify({ gateway: { auth: { token: "config-token" } } }))
359+
360+
const gateway = createServer((req, res) => {
361+
let body = ""
362+
req.on("data", (chunk) => { body += chunk })
363+
req.on("end", () => {
364+
void body
365+
gatewayAuth = req.headers.authorization
366+
res.setHeader("Content-Type", "application/json")
367+
res.end(JSON.stringify({ ok: true, result: { status: "from-config-token" } }))
368+
})
369+
})
370+
await new Promise<void>((resolve) => gateway.listen(0, "127.0.0.1", resolve))
371+
const gatewayAddress = gateway.address()
372+
assert(gatewayAddress && typeof gatewayAddress === "object")
373+
374+
const taas = createServer((req, res) => {
375+
let body = ""
376+
req.on("data", (chunk) => { body += chunk })
377+
req.on("end", () => {
378+
res.setHeader("Content-Type", "application/json")
379+
if (req.url === "/internal/requester-bridges/leases") {
380+
res.end(JSON.stringify({ ok: true, descriptor: {
381+
name: "requester-workspace", version: "2026-05-23", status: "verified",
382+
bridge_id: "br_config", lease_id: "brl_config", capabilities: ["requester.tool.invoke"],
383+
endpoint_ref: "epref_config", auth_context_id: "authctx_config", expires_at: "2026-05-23T19:00:00Z",
384+
} }))
385+
return
386+
}
387+
if (req.url === "/internal/requester-bridges/poll") {
388+
pollCount += 1
389+
res.end(JSON.stringify({ ok: true, operations: pollCount === 1 ? [{
390+
operation_id: "bro_config", audit_id: "bra_config", lease_id: "brl_config", bridge_id: "br_config",
391+
operation: "requester.tool.invoke", arguments: { tool: "session_status", arguments: {} },
392+
}] : [] }))
393+
return
394+
}
395+
if (req.url === "/internal/requester-bridges/results") {
396+
resultBody = JSON.parse(body)
397+
res.end(JSON.stringify({ ok: true }))
398+
return
399+
}
400+
res.statusCode = 404
401+
res.end(JSON.stringify({ ok: false }))
402+
})
403+
})
404+
await new Promise<void>((resolve) => taas.listen(0, "127.0.0.1", resolve))
405+
const taasAddress = taas.address()
406+
assert(taasAddress && typeof taasAddress === "object")
407+
408+
const { plugin, restore } = await loadPlugin({
409+
TAAS_REQUESTER_BRIDGE_POLL_INTERVAL_MS: "50",
410+
TAAS_REQUESTER_LOCAL_GATEWAY_URL: `http://127.0.0.1:${gatewayAddress.port}`,
411+
TAAS_REQUESTER_LOCAL_GATEWAY_TOKEN: undefined,
412+
OPENCLAW_GATEWAY_TOKEN: undefined,
413+
OPENCLAW_GATEWAY_PASSWORD: undefined,
414+
OPENCLAW_STATE_DIR: tempState,
415+
})
416+
try {
417+
await runPayload(captureWrapper(plugin), { messages: [] }, { baseUrl: `http://127.0.0.1:${taasAddress.port}` })
418+
await new Promise((resolve) => setTimeout(resolve, 150))
419+
assert.equal(gatewayAuth, "Bearer config-token")
420+
assert.equal(resultBody.operation_id, "bro_config")
421+
assert.equal(resultBody.ok, true)
422+
assert.deepEqual(resultBody.result, { status: "from-config-token" })
423+
} finally {
424+
restore()
425+
gateway.close()
426+
taas.close()
427+
fs.rmSync(tempState, { recursive: true, force: true })
428+
}
429+
})
430+
349431
test("plugin includes claim_id in result submission when poll returns one", async () => {
350432
let pollCount = 0
351433
let resultBody: any

test/smoke.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ plugin.register({
1212
})
1313

1414
assert.ok(provider, "provider should be registered")
15-
assert.equal(provider.id, "taas-affinity-hook")
16-
assert.deepEqual(provider.hookAliases, ["cloudsigma", "cloudsigma-staging"])
15+
assert.equal(provider.id, "cloudsigma")
16+
assert.deepEqual(provider.hookAliases, ["cloudsigma-staging", "taas-affinity-hook", "openai-completions"])
1717
assert.equal(typeof provider.wrapStreamFn, "function")
1818
assert.equal(typeof provider.resolveTransportTurnState, "function")
1919

0 commit comments

Comments
 (0)