Compare commits

...

1 Commits

Author SHA1 Message Date
Nikhil Sonti
857b563e8b fix: disable bundled OpenClaw gateway auth 2026-05-02 17:13:08 -07:00
13 changed files with 395 additions and 81 deletions

View File

@@ -143,9 +143,8 @@ export async function createHttpServer(config: HttpServerConfig) {
getLimactlPath: () => resolveBundledLimactl(resourcesDir),
getVmName: () => VM_NAME,
},
openclawGatewayChat: new OpenClawGatewayChatClient(
() => getOpenClawService().getPort(),
async () => getOpenClawService().getGatewayToken(),
openclawGatewayChat: new OpenClawGatewayChatClient(() =>
getOpenClawService().getPort(),
),
openclawProvisioner: {
createAgent: (input) => getOpenClawService().createAgent(input),

View File

@@ -53,6 +53,7 @@ export type GatewayContainerSpec = {
hostHome: string
envFilePath: string
gatewayToken?: string
privateIngressNoAuth?: boolean
timezone: string
}
@@ -417,6 +418,9 @@ export class ContainerRuntime {
...(input.gatewayToken
? { OPENCLAW_GATEWAY_TOKEN: input.gatewayToken }
: {}),
...(input.privateIngressNoAuth
? { OPENCLAW_GATEWAY_PRIVATE_INGRESS_NO_AUTH: '1' }
: {}),
}
}

View File

@@ -35,22 +35,23 @@ export interface GatewayChatTurnInput {
signal?: AbortSignal
}
type GatewayTokenProvider = () => Promise<string | null | undefined>
export class OpenClawGatewayChatClient {
constructor(
private readonly getHostPort: () => number,
private readonly getToken: () => Promise<string>,
private readonly getToken?: GatewayTokenProvider,
) {}
async streamTurn(
input: GatewayChatTurnInput,
): Promise<ReadableStream<AgentStreamEvent>> {
const token = await this.getToken()
const response = await fetch(
`http://127.0.0.1:${this.getHostPort()}/v1/chat/completions`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
...(await this.authHeaders()),
'Content-Type': 'application/json',
},
body: JSON.stringify({
@@ -80,6 +81,12 @@ export class OpenClawGatewayChatClient {
},
})
}
private async authHeaders(): Promise<Record<string, string>> {
const token = await this.getToken?.()
const trimmed = token?.trim()
return trimmed ? { Authorization: `Bearer ${trimmed}` } : {}
}
}
function resolveAgentModel(agentId: string): string {

View File

@@ -73,10 +73,12 @@ export type OpenClawSessionHistoryEvent =
}
| { type: 'error'; data: { message: string } }
type GatewayTokenProvider = () => Promise<string | null | undefined>
export class OpenClawHttpClient {
constructor(
private readonly hostPort: number,
private readonly getToken: () => Promise<string>,
private readonly getToken?: GatewayTokenProvider,
) {}
async getSessionHistory(
@@ -103,14 +105,11 @@ export class OpenClawHttpClient {
async isAuthenticated(): Promise<boolean> {
try {
const token = await this.getToken()
const response = await fetch(
`http://127.0.0.1:${this.hostPort}/v1/models`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
headers: await this.authHeaders(),
},
)
return response.ok
@@ -124,13 +123,12 @@ export class OpenClawHttpClient {
input: OpenClawSessionHistoryInput,
extraHeaders: Record<string, string>,
): Promise<Response> {
const token = await this.getToken()
const response = await fetch(
`http://127.0.0.1:${this.hostPort}${buildHistoryPath(sessionKey, input)}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
...(await this.authHeaders()),
...extraHeaders,
},
signal: input.signal,
@@ -149,6 +147,12 @@ export class OpenClawHttpClient {
}
return response
}
private async authHeaders(): Promise<Record<string, string>> {
const token = await this.getToken?.()
const trimmed = token?.trim()
return trimmed ? { Authorization: `Bearer ${trimmed}` } : {}
}
}
function buildHistoryPath(

View File

@@ -54,10 +54,10 @@ export class OpenClawObserver {
constructor(private readonly session: ClawSession) {}
/** Start observing the gateway at the given URL with the given token. */
connect(gatewayUrl: string, token: string): void {
/** Start observing the gateway at the given URL. */
connect(gatewayUrl: string, token?: string | null): void {
this.gatewayUrl = gatewayUrl
this.gatewayToken = token
this.gatewayToken = token?.trim() || null
this.closed = false
this.doConnect()
}
@@ -83,7 +83,7 @@ export class OpenClawObserver {
// ── Private ─────────────────────────────────────────────────────────
private doConnect(): void {
if (this.closed || !this.gatewayUrl || !this.gatewayToken) return
if (this.closed || !this.gatewayUrl) return
const wsUrl = this.gatewayUrl
.replace(/^http:\/\//, 'ws://')
@@ -101,6 +101,37 @@ export class OpenClawObserver {
let handshakeSent = false
/**
* Send the gateway protocol connect frame. BrowserOS no-auth gateways omit
* auth entirely; legacy token-mode gateways can still pass a token in.
*/
const sendConnectRequest = () => {
if (handshakeSent) return
handshakeSent = true
const connectReq: RequestFrame = {
type: 'req',
id: HANDSHAKE_REQUEST_ID,
method: 'connect',
params: {
minProtocol: PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
client: {
id: 'openclaw-tui',
displayName: 'browseros-observer',
version: '1.0.0',
platform: 'node',
mode: 'ui',
},
role: 'operator',
scopes: ['operator.read'],
...(this.gatewayToken ? { auth: { token: this.gatewayToken } } : {}),
},
}
ws.send(JSON.stringify(connectReq))
}
ws.on('open', sendConnectRequest)
ws.on('message', (raw) => {
let frame: IncomingFrame
try {
@@ -109,34 +140,14 @@ export class OpenClawObserver {
return
}
// The gateway sends a connect.challenge event before accepting
// the connect request. Send the handshake after receiving it.
// Older gateway builds emit connect.challenge before the connect
// response; keep this path so the observer tolerates both flows.
if (
frame.type === 'event' &&
frame.event === 'connect.challenge' &&
!handshakeSent
) {
handshakeSent = true
const connectReq: RequestFrame = {
type: 'req',
id: HANDSHAKE_REQUEST_ID,
method: 'connect',
params: {
minProtocol: PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
client: {
id: 'openclaw-tui',
displayName: 'browseros-observer',
version: '1.0.0',
platform: 'node',
mode: 'ui',
},
role: 'operator',
scopes: ['operator.read'],
auth: { token: this.gatewayToken },
},
}
ws.send(JSON.stringify(connectReq))
sendConnectRequest()
return
}

View File

@@ -262,6 +262,7 @@ export class OpenClawService {
private hostPort = OPENCLAW_GATEWAY_CONTAINER_PORT
private token: string
private tokenLoaded = false
private gatewayAuthMode: 'unknown' | 'none' | 'token' | 'password' = 'unknown'
private lastError: string | null = null
private browserosServerPort: number
private resourcesDir: string | null
@@ -284,9 +285,8 @@ export class OpenClawService {
this.token = crypto.randomUUID()
this.cliClient = new OpenClawCliClient(this.runtime)
this.bootstrapCliClient = this.buildBootstrapCliClient()
this.httpClient = new OpenClawHttpClient(
this.hostPort,
async () => this.token,
this.httpClient = new OpenClawHttpClient(this.hostPort, () =>
this.getGatewayHttpToken(),
)
this.browserosServerPort =
config.browserosServerPort ?? DEFAULT_PORTS.server
@@ -324,13 +324,9 @@ export class OpenClawService {
}
/**
* Current gateway auth token. The token string is loaded from
* `gateway.auth.token` in the persisted openclaw.json during setup,
* with a freshly generated UUID as fallback. Exposed so the ACPx
* harness can pass it to spawned `openclaw acp` child processes via
* the documented `OPENCLAW_GATEWAY_TOKEN` env var (avoids both the
* `--token` process-listing leak and reliance on a token-file path
* that doesn't exist as a discrete file inside the container).
* Legacy gateway auth token accessor. BrowserOS configures new bundled
* gateways with `gateway.auth.mode=none`; this remains for older token-auth
* gateway clients that still ask the service for a token.
*/
getGatewayToken(): string {
return this.token
@@ -401,7 +397,7 @@ export class OpenClawService {
await this.bootstrapCliClient.runOnboard({
acceptRisk: true,
authChoice: 'skip',
gatewayAuth: 'token',
gatewayAuth: 'none',
gatewayBind: 'lan',
gatewayPort: OPENCLAW_GATEWAY_CONTAINER_PORT,
installDaemon: false,
@@ -1001,9 +997,8 @@ export class OpenClawService {
private setPort(hostPort: number): void {
if (hostPort === this.hostPort) return
this.hostPort = hostPort
this.httpClient = new OpenClawHttpClient(
this.hostPort,
async () => this.token,
this.httpClient = new OpenClawHttpClient(this.hostPort, () =>
this.getGatewayHttpToken(),
)
}
@@ -1037,24 +1032,15 @@ export class OpenClawService {
}
private async isGatewayAuthenticated(hostPort: number): Promise<boolean> {
if (!this.tokenLoaded) {
logger.debug(
'OpenClaw gateway port is ready before auth token is loaded',
{
hostPort,
},
)
return false
}
const client =
hostPort === this.hostPort
? this.httpClient
: new OpenClawHttpClient(hostPort, async () => this.token)
: new OpenClawHttpClient(hostPort, () => this.getGatewayHttpToken())
const authenticated = await client.isAuthenticated()
if (!authenticated) {
logger.warn('OpenClaw gateway port rejected current auth token', {
logger.warn('OpenClaw gateway readiness probe failed', {
hostPort,
authMode: this.gatewayAuthMode,
})
}
return authenticated
@@ -1118,7 +1104,9 @@ export class OpenClawService {
// ClawSession starts empty after the JSONL seed was removed; the WS
// observer fills in agent status as events arrive.
const url = `http://127.0.0.1:${this.hostPort}`
this.observer.connect(url, this.token)
const token =
this.gatewayAuthMode === 'token' && this.tokenLoaded ? this.token : null
this.observer.connect(url, token)
}
private classifyControlPlaneError(
@@ -1354,7 +1342,11 @@ export class OpenClawService {
hostPort: this.hostPort,
hostHome: this.openclawDir,
envFilePath: this.getStateEnvPath(),
gatewayToken: this.tokenLoaded ? this.token : undefined,
gatewayToken:
this.gatewayAuthMode === 'token' && this.tokenLoaded
? this.token
: undefined,
privateIngressNoAuth: this.gatewayAuthMode === 'none',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}
}
@@ -1460,7 +1452,7 @@ export class OpenClawService {
}
private async ensureTokenLoaded(): Promise<void> {
if (this.tokenLoaded) {
if (this.gatewayAuthMode !== 'unknown') {
return
}
if (!existsSync(this.getStateConfigPath())) {
@@ -1472,6 +1464,7 @@ export class OpenClawService {
private async refreshGatewayAuthToken(): Promise<void> {
this.tokenLoaded = false
this.gatewayAuthMode = 'unknown'
if (!existsSync(this.getStateConfigPath())) {
return
}
@@ -1486,16 +1479,28 @@ export class OpenClawService {
) as {
gateway?: {
auth?: {
mode?: unknown
token?: unknown
}
}
}
const token = config.gateway?.auth?.token
const auth = config.gateway?.auth
const mode = auth?.mode
if (mode === 'none') {
this.gatewayAuthMode = 'none'
logger.debug('OpenClaw gateway config uses no auth')
return
}
const token = auth?.token
if (typeof token === 'string' && token) {
this.token = token
this.tokenLoaded = true
this.gatewayAuthMode = 'token'
logger.info('Loaded OpenClaw gateway token from mounted config')
return
}
this.gatewayAuthMode = mode === 'password' ? 'password' : 'none'
} catch (err) {
logger.warn('Failed to load OpenClaw gateway token from mounted config', {
error: err instanceof Error ? err.message : String(err),
@@ -1503,6 +1508,13 @@ export class OpenClawService {
}
}
private async getGatewayHttpToken(): Promise<string | null> {
await this.ensureTokenLoaded()
return this.gatewayAuthMode === 'token' && this.tokenLoaded
? this.token
: null
}
private createProgressLogger(
onLog?: (msg: string) => void,
): (msg: string) => void {

View File

@@ -67,7 +67,7 @@ import type {
* current token and VM/container paths at spawn time.
*/
export interface OpenclawGatewayAccessor {
/** Current gateway auth token. Passed to `openclaw acp --token`. */
/** Current gateway auth token. Kept for legacy token-auth gateway clients. */
getGatewayToken(): string
/** Container name e.g. browseros-openclaw-openclaw-gateway-1. */
getContainerName(): string
@@ -1000,8 +1000,8 @@ function createBrowserosAgentRegistry(input: {
* already installed alongside the gateway is reused; BrowserOS does
* not require a host-side openclaw install.
*
* Auth: `openclaw acp --url ...` deliberately does not reuse implicit
* env/config credentials, so pass the gateway token explicitly.
* Auth: BrowserOS configures the bundled gateway with `gateway.auth.mode=none`,
* so no gateway token flag is needed for the local ACP bridge.
*
* Banner output: OPENCLAW_HIDE_BANNER and OPENCLAW_SUPPRESS_NOTES
* suppress non-JSON-RPC chatter on stdout that would otherwise corrupt
@@ -1011,7 +1011,6 @@ function resolveOpenclawAcpCommand(
gateway: OpenclawGatewayAccessor,
sessionKey: string | null,
): string {
const token = gateway.getGatewayToken()
const limactl = gateway.getLimactlPath()
const vm = gateway.getVmName()
const container = gateway.getContainerName()
@@ -1060,8 +1059,6 @@ function resolveOpenclawAcpCommand(
'acp',
'--url',
gatewayUrlInsideContainer,
'--token',
token,
]
if (bridgeSessionKey) {
argv.push('--session', bridgeSessionKey)

View File

@@ -159,6 +159,31 @@ describe('ContainerRuntime', () => {
)
})
it('passes private-ingress no-auth only when requested', async () => {
const deps = createDeps()
const runtime = new ContainerRuntime({
vm: deps.vm,
shell: deps.shell,
loader: deps.loader,
projectDir: PROJECT_DIR,
})
await runtime.startGateway({
...defaultSpec,
gatewayToken: undefined,
privateIngressNoAuth: true,
})
expect(deps.shell.createContainer).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.objectContaining({
OPENCLAW_GATEWAY_PRIVATE_INGRESS_NO_AUTH: '1',
}),
}),
undefined,
)
})
it('delegates ensureReady and stopVm to VmRuntime', async () => {
const deps = createDeps()
const runtime = new ContainerRuntime({

View File

@@ -0,0 +1,89 @@
/**
* @license
* Copyright 2025 BrowserOS
*/
import { afterEach, describe, expect, it, mock } from 'bun:test'
import { OpenClawGatewayChatClient } from '../../../../src/api/services/openclaw/openclaw-gateway-chat-client'
describe('OpenClawGatewayChatClient', () => {
const originalFetch = globalThis.fetch
afterEach(() => {
globalThis.fetch = originalFetch
})
it('posts chat completions without Authorization when no token provider is configured', async () => {
const fetchMock = mock(() =>
Promise.resolve(
new Response(emptyStream(), {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
}),
),
)
globalThis.fetch = fetchMock as typeof globalThis.fetch
const client = new OpenClawGatewayChatClient(() => 18794)
await client.streamTurn({
agentId: 'main',
sessionKey: 'main',
messages: [{ role: 'user', content: 'hi' }],
})
expect(fetchMock.mock.calls[0]?.[0]).toBe(
'http://127.0.0.1:18794/v1/chat/completions',
)
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
expect(fetchHeaders(fetchMock)).not.toHaveProperty('Authorization')
})
it('keeps bearer auth for legacy token-auth gateways', async () => {
const fetchMock = mock(() =>
Promise.resolve(
new Response(emptyStream(), {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
}),
),
)
globalThis.fetch = fetchMock as typeof globalThis.fetch
const client = new OpenClawGatewayChatClient(
() => 18794,
async () => 'gateway-token',
)
await client.streamTurn({
agentId: 'ops',
sessionKey: 'main',
messages: [{ role: 'user', content: 'hi' }],
})
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
headers: {
Authorization: 'Bearer gateway-token',
'Content-Type': 'application/json',
},
})
})
})
function emptyStream(): ReadableStream<Uint8Array> {
return new ReadableStream({
start(controller) {
controller.close()
},
})
}
function fetchHeaders(
fetchMock: ReturnType<typeof mock>,
): Record<string, string> {
return ((fetchMock.mock.calls[0]?.[1] as RequestInit | undefined)?.headers ??
{}) as Record<string, string>
}

View File

@@ -32,6 +32,22 @@ describe('OpenClawHttpClient', () => {
})
})
it('checks no-auth gateway availability without an Authorization header', async () => {
const fetchMock = mock(() => Promise.resolve(new Response('{}')))
globalThis.fetch = fetchMock as typeof globalThis.fetch
const client = new OpenClawHttpClient(18789)
await expect(client.isAuthenticated()).resolves.toBe(true)
expect(fetchMock.mock.calls[0]?.[0]).toBe(
'http://127.0.0.1:18789/v1/models',
)
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'GET',
})
expect(fetchHeaders(fetchMock)).not.toHaveProperty('Authorization')
})
it('treats rejected gateway authentication as unavailable', async () => {
globalThis.fetch = mock(() =>
Promise.resolve(new Response('Unauthorized', { status: 401 })),
@@ -94,6 +110,25 @@ describe('OpenClawHttpClient', () => {
})
})
it('sends no Authorization header when no token provider is configured', async () => {
const fetchMock = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ sessionKey: 'k', messages: [] }), {
status: 200,
}),
),
)
globalThis.fetch = fetchMock as typeof globalThis.fetch
const client = new OpenClawHttpClient(18789)
await client.getSessionHistory('k')
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'GET',
})
expect(fetchHeaders(fetchMock)).not.toHaveProperty('Authorization')
})
it('omits limit and cursor from the query when undefined', async () => {
const fetchMock = mock(() =>
Promise.resolve(
@@ -215,6 +250,33 @@ describe('OpenClawHttpClient', () => {
])
})
it('keeps SSE Accept without Authorization when no token provider is configured', async () => {
const fetchMock = mock(() =>
Promise.resolve(
new Response(
new ReadableStream({
start(controller) {
controller.close()
},
}),
{ status: 200 },
),
),
)
globalThis.fetch = fetchMock as typeof globalThis.fetch
const client = new OpenClawHttpClient(18789)
await client.streamSessionHistory('k')
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'GET',
headers: {
Accept: 'text/event-stream',
},
})
expect(fetchHeaders(fetchMock)).not.toHaveProperty('Authorization')
})
it('forwards upstream error frames and closes', async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
@@ -315,3 +377,10 @@ async function readEvents(
return events
}
function fetchHeaders(
fetchMock: ReturnType<typeof mock>,
): Record<string, string> {
return ((fetchMock.mock.calls[0]?.[1] as RequestInit | undefined)?.headers ??
{}) as Record<string, string>
}

View File

@@ -338,7 +338,7 @@ describe('OpenClawService', () => {
expect(runOnboard).toHaveBeenCalledWith({
acceptRisk: true,
authChoice: 'skip',
gatewayAuth: 'token',
gatewayAuth: 'none',
gatewayBind: 'lan',
gatewayPort: 18789,
installDaemon: false,
@@ -680,6 +680,49 @@ describe('OpenClawService', () => {
expect(probe).toHaveBeenCalledTimes(1)
})
it('start ignores stale gateway tokens when config auth mode is none', async () => {
tempDir = await mkdtemp(join(tmpdir(), 'openclaw-service-'))
await mkdir(join(tempDir, '.openclaw'), { recursive: true })
await writeFile(
join(tempDir, '.openclaw', 'openclaw.json'),
JSON.stringify({
gateway: {
auth: {
mode: 'none',
token: 'stale-token',
},
},
}),
)
const ensureReady = mock(async () => {})
const startGateway = mock(async () => {})
const waitForReady = mock(async () => true)
const probe = mock(async () => {})
const service = new OpenClawService() as MutableOpenClawService
service.openclawDir = tempDir
service.runtime = {
ensureReady,
isReady: async () => false,
startGateway,
waitForReady,
}
service.cliClient = {
probe,
}
await service.start()
expect(startGateway).toHaveBeenCalledWith(
expect.objectContaining({
gatewayToken: undefined,
privateIngressNoAuth: true,
}),
expect.any(Function),
)
expect(service.token).not.toBe('stale-token')
})
it('serializes concurrent start calls and only starts the gateway once', async () => {
tempDir = await mkdtemp(join(tmpdir(), 'openclaw-service-'))
await mkdir(join(tempDir, '.openclaw'), { recursive: true })
@@ -1136,6 +1179,53 @@ describe('OpenClawService', () => {
expect(probe).toHaveBeenCalledTimes(1)
})
it('tryAutoStart reuses a ready no-auth gateway without Authorization', async () => {
tempDir = await mkdtemp(join(tmpdir(), 'openclaw-service-'))
await mkdir(join(tempDir, '.openclaw'), { recursive: true })
await writeFile(
join(tempDir, '.openclaw', 'openclaw.json'),
JSON.stringify({
gateway: {
auth: {
mode: 'none',
token: 'stale-token',
},
},
}),
)
const ensureReady = mock(async () => {})
const isReady = mock(async () => true)
const isGatewayCurrent = mock(async () => true)
const startGateway = mock(async () => {})
const probe = mock(async () => {})
const fetchMock = mock(() =>
Promise.resolve(new Response('', { status: 200 })),
)
globalThis.fetch = fetchMock as typeof globalThis.fetch
const service = new OpenClawService() as MutableOpenClawService
service.openclawDir = tempDir
service.runtime = {
ensureReady,
isReady,
isGatewayCurrent,
startGateway,
}
service.cliClient = { probe }
await service.tryAutoStart()
expect(startGateway).not.toHaveBeenCalled()
expect(fetchMock.mock.calls[0]?.[0]).toBe(
'http://127.0.0.1:18789/v1/models',
)
expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'GET',
})
expect(fetchHeaders(fetchMock)).not.toHaveProperty('Authorization')
expect(probe).toHaveBeenCalledTimes(1)
})
it('tryAutoStart recreates a ready gateway when the image is stale', async () => {
tempDir = await mkdtemp(join(tmpdir(), 'openclaw-service-'))
await mkdir(join(tempDir, '.openclaw'), { recursive: true })
@@ -1720,3 +1810,10 @@ function mockGatewayAuth(status = 200): ReturnType<typeof mock> {
globalThis.fetch = fetchMock as typeof globalThis.fetch
return fetchMock
}
function fetchHeaders(
fetchMock: ReturnType<typeof mock>,
): Record<string, string> {
return ((fetchMock.mock.calls[0]?.[1] as RequestInit | undefined)?.headers ??
{}) as Record<string, string>
}

View File

@@ -1023,9 +1023,8 @@ Use the BrowserOS MCP server for all browser tasks, including browsing the web,
expect(command).toContain(
'nerdctl exec -i -e OPENCLAW_HIDE_BANNER=1 -e OPENCLAW_SUPPRESS_NOTES=1 browseros-openclaw-openclaw-gateway-1',
)
expect(command).toContain(
'openclaw acp --url ws://127.0.0.1:18789 --token test-token-abc',
)
expect(command).toContain('openclaw acp --url ws://127.0.0.1:18789')
expect(command).not.toContain('--token')
// sessionKey routing: the bridge needs --session <key> to map newSession
// requests to the matching gateway agent (acpx does not forward
// sessionKey via ACP newSession params).

View File

@@ -1,5 +1,6 @@
export const OPENCLAW_AGENT_NAME = 'openclaw'
export const OPENCLAW_IMAGE = 'ghcr.io/openclaw/openclaw:2026.4.12'
export const OPENCLAW_IMAGE =
'ghcr.io/browseros-ai/openclaw:2026.5.2-browseros.1'
export const OPENCLAW_GATEWAY_CONTAINER_PORT = 18789
export const OPENCLAW_CONTAINER_HOME = '/home/node/.openclaw'
export const OPENCLAW_COMPOSE_PROJECT_NAME = 'browseros-openclaw'