feat: support configurable gateway port
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
|
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
|
||||||
|
|
||||||
|
|||||||
@@ -106,4 +106,20 @@ enum ClawdisConfigFile {
|
|||||||
return remote["password"] as? String
|
return remote["password"] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func gatewayPort() -> Int? {
|
||||||
|
let root = self.loadDict()
|
||||||
|
guard let gateway = root["gateway"] as? [String: Any] else { return nil }
|
||||||
|
if let port = gateway["port"] as? Int, port > 0 { return port }
|
||||||
|
if let number = gateway["port"] as? NSNumber, number.intValue > 0 {
|
||||||
|
return number.intValue
|
||||||
|
}
|
||||||
|
if let raw = gateway["port"] as? String,
|
||||||
|
let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)),
|
||||||
|
parsed > 0
|
||||||
|
{
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ final class ControlChannel {
|
|||||||
"Reason: \(reason)"
|
"Reason: \(reason)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common misfire: we connected to localhost:18789 but the port is occupied
|
// Common misfire: we connected to the configured localhost port but it is occupied
|
||||||
// by some other process (e.g. a local dev gateway or a stuck SSH forward).
|
// by some other process (e.g. a local dev gateway or a stuck SSH forward).
|
||||||
// The gateway handshake returns something we can't parse, which currently
|
// The gateway handshake returns something we can't parse, which currently
|
||||||
// surfaces as "hello failed (unexpected response)". Give the user a pointer
|
// surfaces as "hello failed (unexpected response)". Give the user a pointer
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ struct DebugSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.portReports.isEmpty, !self.portCheckInFlight {
|
if self.portReports.isEmpty, !self.portCheckInFlight {
|
||||||
Text("Check which process owns 18789 and suggest fixes.")
|
Text("Check which process owns \(GatewayEnvironment.gatewayPort()) and suggest fixes.")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
} else {
|
} else {
|
||||||
@@ -946,7 +946,7 @@ extension DebugSettings {
|
|||||||
view.portCheckInFlight = true
|
view.portCheckInFlight = true
|
||||||
view.portReports = [
|
view.portReports = [
|
||||||
DebugActions.PortReport(
|
DebugActions.PortReport(
|
||||||
port: 18789,
|
port: GatewayEnvironment.gatewayPort(),
|
||||||
expected: "Gateway websocket (node/tsx)",
|
expected: "Gateway websocket (node/tsx)",
|
||||||
status: .missing("Missing"),
|
status: .missing("Missing"),
|
||||||
listeners: []),
|
listeners: []),
|
||||||
|
|||||||
@@ -72,6 +72,13 @@ enum GatewayEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func gatewayPort() -> Int {
|
static func gatewayPort() -> Int {
|
||||||
|
if let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PORT"] {
|
||||||
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if let parsed = Int(trimmed), parsed > 0 { return parsed }
|
||||||
|
}
|
||||||
|
if let configPort = ClawdisConfigFile.gatewayPort(), configPort > 0 {
|
||||||
|
return configPort
|
||||||
|
}
|
||||||
let stored = UserDefaults.standard.integer(forKey: "gatewayPort")
|
let stored = UserDefaults.standard.integer(forKey: "gatewayPort")
|
||||||
return stored > 0 ? stored : 18789
|
return stored > 0 ? stored : 18789
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,8 @@ final class HealthStore {
|
|||||||
if let error = self.lastError, !error.isEmpty {
|
if let error = self.lastError, !error.isEmpty {
|
||||||
let lower = error.lowercased()
|
let lower = error.lowercased()
|
||||||
if lower.contains("connection refused") {
|
if lower.contains("connection refused") {
|
||||||
return "The gateway control port (127.0.0.1:18789) isn’t listening — restart Clawdis to bring it back."
|
let port = GatewayEnvironment.gatewayPort()
|
||||||
|
return "The gateway control port (127.0.0.1:\(port)) isn’t listening — restart Clawdis to bring it back."
|
||||||
}
|
}
|
||||||
if lower.contains("timeout") {
|
if lower.contains("timeout") {
|
||||||
return "Timed out waiting for the control server; the gateway may be crashed or still starting."
|
return "Timed out waiting for the control server; the gateway may be crashed or still starting."
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ extension OnboardingView {
|
|||||||
discoveryModel: discovery)
|
discoveryModel: discovery)
|
||||||
view.needsBootstrap = true
|
view.needsBootstrap = true
|
||||||
view.localGatewayProbe = LocalGatewayProbe(
|
view.localGatewayProbe = LocalGatewayProbe(
|
||||||
port: 18789,
|
port: GatewayEnvironment.gatewayPort(),
|
||||||
pid: 123,
|
pid: 123,
|
||||||
command: "clawdis-gateway",
|
command: "clawdis-gateway",
|
||||||
expected: true)
|
expected: true)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ actor PortGuardian {
|
|||||||
self.logger.info("port sweep skipped (mode=unconfigured)")
|
self.logger.info("port sweep skipped (mode=unconfigured)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let ports = [18789]
|
let ports = [GatewayEnvironment.gatewayPort()]
|
||||||
for port in ports {
|
for port in ports {
|
||||||
let listeners = await self.listeners(on: port)
|
let listeners = await self.listeners(on: port)
|
||||||
guard !listeners.isEmpty else { continue }
|
guard !listeners.isEmpty else { continue }
|
||||||
@@ -148,7 +148,7 @@ actor PortGuardian {
|
|||||||
if mode == .unconfigured {
|
if mode == .unconfigured {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let ports = [18789]
|
let ports = [GatewayEnvironment.gatewayPort()]
|
||||||
var reports: [PortReport] = []
|
var reports: [PortReport] = []
|
||||||
|
|
||||||
for port in ports {
|
for port in ports {
|
||||||
@@ -279,7 +279,8 @@ actor PortGuardian {
|
|||||||
return .init(port: port, expected: expectedDesc, status: .missing(text), listeners: [])
|
return .init(port: port, expected: expectedDesc, status: .missing(text), listeners: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
let tunnelUnhealthy = mode == .remote && port == 18789 && tunnelHealthy == false
|
let tunnelUnhealthy =
|
||||||
|
mode == .remote && port == GatewayEnvironment.gatewayPort() && tunnelHealthy == false
|
||||||
let reportListeners = listeners.map { listener in
|
let reportListeners = listeners.map { listener in
|
||||||
var expected = okPredicate(listener)
|
var expected = okPredicate(listener)
|
||||||
if tunnelUnhealthy, expected { expected = false }
|
if tunnelUnhealthy, expected { expected = false }
|
||||||
@@ -347,7 +348,7 @@ actor PortGuardian {
|
|||||||
switch mode {
|
switch mode {
|
||||||
case .remote:
|
case .remote:
|
||||||
// Remote mode expects an SSH tunnel for the gateway WebSocket port.
|
// Remote mode expects an SSH tunnel for the gateway WebSocket port.
|
||||||
if port == 18789 { return cmd.contains("ssh") }
|
if port == GatewayEnvironment.gatewayPort() { return cmd.contains("ssh") }
|
||||||
return false
|
return false
|
||||||
case .local:
|
case .local:
|
||||||
return expectedCommands.contains { cmd.contains($0) }
|
return expectedCommands.contains { cmd.contains($0) }
|
||||||
@@ -361,7 +362,7 @@ actor PortGuardian {
|
|||||||
mode: AppState.ConnectionMode,
|
mode: AppState.ConnectionMode,
|
||||||
listeners: [Listener]) async -> Bool?
|
listeners: [Listener]) async -> Bool?
|
||||||
{
|
{
|
||||||
guard mode == .remote, port == 18789, !listeners.isEmpty else { return nil }
|
guard mode == .remote, port == GatewayEnvironment.gatewayPort(), !listeners.isEmpty else { return nil }
|
||||||
let hasSsh = listeners.contains { $0.command.lowercased().contains("ssh") }
|
let hasSsh = listeners.contains { $0.command.lowercased().contains("ssh") }
|
||||||
guard hasSsh else { return nil }
|
guard hasSsh else { return nil }
|
||||||
return await self.probeGatewayHealth(port: port)
|
return await self.probeGatewayHealth(port: port)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ actor RemoteTunnelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure an SSH tunnel is running for the gateway control port.
|
/// Ensure an SSH tunnel is running for the gateway control port.
|
||||||
/// Returns the local forwarded port (usually 18789).
|
/// Returns the local forwarded port (usually the configured gateway port).
|
||||||
func ensureControlTunnel() async throws -> UInt16 {
|
func ensureControlTunnel() async throws -> UInt16 {
|
||||||
let settings = CommandResolver.connectionSettings()
|
let settings = CommandResolver.connectionSettings()
|
||||||
guard settings.mode == .remote else {
|
guard settings.mode == .remote else {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { CONFIG_PATH_CLAWDIS, loadConfig } from "../config/config.js";
|
import {
|
||||||
|
CONFIG_PATH_CLAWDIS,
|
||||||
|
loadConfig,
|
||||||
|
resolveGatewayPort,
|
||||||
|
} from "../config/config.js";
|
||||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||||
import { startGatewayServer } from "../gateway/server.js";
|
import { startGatewayServer } from "../gateway/server.js";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +31,13 @@ const gatewayLog = createSubsystemLogger("gateway");
|
|||||||
|
|
||||||
type GatewayRunSignalAction = "stop" | "restart";
|
type GatewayRunSignalAction = "stop" | "restart";
|
||||||
|
|
||||||
|
function parsePort(raw: unknown): number | null {
|
||||||
|
if (raw === undefined || raw === null) return null;
|
||||||
|
const parsed = Number.parseInt(String(raw), 10);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
async function runGatewayLoop(params: {
|
async function runGatewayLoop(params: {
|
||||||
start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;
|
start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;
|
||||||
runtime: typeof defaultRuntime;
|
runtime: typeof defaultRuntime;
|
||||||
@@ -186,8 +197,15 @@ export function registerGatewayCli(program: Command) {
|
|||||||
}
|
}
|
||||||
setGatewayWsLogStyle(wsLogStyle);
|
setGatewayWsLogStyle(wsLogStyle);
|
||||||
|
|
||||||
const port = Number.parseInt(String(opts.port ?? "18789"), 10);
|
const cfg = loadConfig();
|
||||||
if (Number.isNaN(port) || port <= 0) {
|
const portOverride = parsePort(opts.port);
|
||||||
|
if (opts.port !== undefined && portOverride === null) {
|
||||||
|
defaultRuntime.error("Invalid port");
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const port = portOverride ?? resolveGatewayPort(cfg);
|
||||||
|
if (!Number.isFinite(port) || port <= 0) {
|
||||||
defaultRuntime.error("Invalid port");
|
defaultRuntime.error("Invalid port");
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
return;
|
return;
|
||||||
@@ -219,7 +237,6 @@ export function registerGatewayCli(program: Command) {
|
|||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cfg = loadConfig();
|
|
||||||
const bindRaw = String(opts.bind ?? cfg.gateway?.bind ?? "loopback");
|
const bindRaw = String(opts.bind ?? cfg.gateway?.bind ?? "loopback");
|
||||||
const bind =
|
const bind =
|
||||||
bindRaw === "loopback" ||
|
bindRaw === "loopback" ||
|
||||||
@@ -335,8 +352,14 @@ export function registerGatewayCli(program: Command) {
|
|||||||
}
|
}
|
||||||
setGatewayWsLogStyle(wsLogStyle);
|
setGatewayWsLogStyle(wsLogStyle);
|
||||||
|
|
||||||
const port = Number.parseInt(String(opts.port ?? "18789"), 10);
|
const cfg = loadConfig();
|
||||||
if (Number.isNaN(port) || port <= 0) {
|
const portOverride = parsePort(opts.port);
|
||||||
|
if (opts.port !== undefined && portOverride === null) {
|
||||||
|
defaultRuntime.error("Invalid port");
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
const port = portOverride ?? resolveGatewayPort(cfg);
|
||||||
|
if (!Number.isFinite(port) || port <= 0) {
|
||||||
defaultRuntime.error("Invalid port");
|
defaultRuntime.error("Invalid port");
|
||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
@@ -400,7 +423,6 @@ export function registerGatewayCli(program: Command) {
|
|||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cfg = loadConfig();
|
|
||||||
const configExists = fs.existsSync(CONFIG_PATH_CLAWDIS);
|
const configExists = fs.existsSync(CONFIG_PATH_CLAWDIS);
|
||||||
const mode = cfg.gateway?.mode;
|
const mode = cfg.gateway?.mode;
|
||||||
if (!opts.allowUnconfigured && mode !== "local") {
|
if (!opts.allowUnconfigured && mode !== "local") {
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function buildProgram() {
|
|||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option("--auth-choice <choice>", "Auth: oauth|apiKey|minimax|skip")
|
.option("--auth-choice <choice>", "Auth: oauth|apiKey|minimax|skip")
|
||||||
.option("--anthropic-api-key <key>", "Anthropic API key")
|
.option("--anthropic-api-key <key>", "Anthropic API key")
|
||||||
.option("--gateway-port <port>", "Gateway port", "18789")
|
.option("--gateway-port <port>", "Gateway port")
|
||||||
.option("--gateway-bind <mode>", "Gateway bind: loopback|lan|tailnet|auto")
|
.option("--gateway-bind <mode>", "Gateway bind: loopback|lan|tailnet|auto")
|
||||||
.option("--gateway-auth <mode>", "Gateway auth: off|token|password")
|
.option("--gateway-auth <mode>", "Gateway auth: off|token|password")
|
||||||
.option("--gateway-token <token>", "Gateway token (token auth)")
|
.option("--gateway-token <token>", "Gateway token (token auth)")
|
||||||
@@ -194,10 +194,10 @@ export function buildProgram() {
|
|||||||
| "skip"
|
| "skip"
|
||||||
| undefined,
|
| undefined,
|
||||||
anthropicApiKey: opts.anthropicApiKey as string | undefined,
|
anthropicApiKey: opts.anthropicApiKey as string | undefined,
|
||||||
gatewayPort: Number.parseInt(
|
gatewayPort:
|
||||||
String(opts.gatewayPort ?? "18789"),
|
typeof opts.gatewayPort === "string"
|
||||||
10,
|
? Number.parseInt(opts.gatewayPort, 10)
|
||||||
),
|
: undefined,
|
||||||
gatewayBind: opts.gatewayBind as
|
gatewayBind: opts.gatewayBind as
|
||||||
| "loopback"
|
| "loopback"
|
||||||
| "lan"
|
| "lan"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { ClawdisConfig } from "../config/config.js";
|
|||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
|
resolveGatewayPort,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||||
@@ -74,7 +75,7 @@ async function promptGatewayConfig(
|
|||||||
const portRaw = guardCancel(
|
const portRaw = guardCancel(
|
||||||
await text({
|
await text({
|
||||||
message: "Gateway port",
|
message: "Gateway port",
|
||||||
initialValue: "18789",
|
initialValue: String(resolveGatewayPort(cfg)),
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||||
}),
|
}),
|
||||||
@@ -205,6 +206,7 @@ async function promptGatewayConfig(
|
|||||||
gateway: {
|
gateway: {
|
||||||
...next.gateway,
|
...next.gateway,
|
||||||
mode: "local",
|
mode: "local",
|
||||||
|
port,
|
||||||
bind,
|
bind,
|
||||||
tailscale: {
|
tailscale: {
|
||||||
...next.gateway?.tailscale,
|
...next.gateway?.tailscale,
|
||||||
@@ -527,7 +529,7 @@ export async function runConfigureWizard(
|
|||||||
nextConfig.agent?.workspace ??
|
nextConfig.agent?.workspace ??
|
||||||
baseConfig.agent?.workspace ??
|
baseConfig.agent?.workspace ??
|
||||||
DEFAULT_WORKSPACE;
|
DEFAULT_WORKSPACE;
|
||||||
let gatewayPort = 18789;
|
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||||
let gatewayToken: string | undefined;
|
let gatewayToken: string | undefined;
|
||||||
|
|
||||||
if (selected.includes("workspace")) {
|
if (selected.includes("workspace")) {
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export function summarizeExistingConfig(config: ClawdisConfig): string {
|
|||||||
rows.push(`workspace: ${config.agent.workspace}`);
|
rows.push(`workspace: ${config.agent.workspace}`);
|
||||||
if (config.agent?.model) rows.push(`model: ${config.agent.model}`);
|
if (config.agent?.model) rows.push(`model: ${config.agent.model}`);
|
||||||
if (config.gateway?.mode) rows.push(`gateway.mode: ${config.gateway.mode}`);
|
if (config.gateway?.mode) rows.push(`gateway.mode: ${config.gateway.mode}`);
|
||||||
|
if (typeof config.gateway?.port === "number") {
|
||||||
|
rows.push(`gateway.port: ${config.gateway.port}`);
|
||||||
|
}
|
||||||
if (config.gateway?.bind) rows.push(`gateway.bind: ${config.gateway.bind}`);
|
if (config.gateway?.bind) rows.push(`gateway.bind: ${config.gateway.bind}`);
|
||||||
if (config.gateway?.remote?.url) {
|
if (config.gateway?.remote?.url) {
|
||||||
rows.push(`gateway.remote.url: ${config.gateway.remote.url}`);
|
rows.push(`gateway.remote.url: ${config.gateway.remote.url}`);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type { ClawdisConfig } from "../config/config.js";
|
|||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
|
resolveGatewayPort,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||||
@@ -118,7 +119,8 @@ export async function runInteractiveOnboarding(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const localUrl = "ws://127.0.0.1:18789";
|
const localPort = resolveGatewayPort(baseConfig);
|
||||||
|
const localUrl = `ws://127.0.0.1:${localPort}`;
|
||||||
const localProbe = await probeGatewayReachable({
|
const localProbe = await probeGatewayReachable({
|
||||||
url: localUrl,
|
url: localUrl,
|
||||||
token: process.env.CLAWDIS_GATEWAY_TOKEN,
|
token: process.env.CLAWDIS_GATEWAY_TOKEN,
|
||||||
@@ -315,7 +317,7 @@ export async function runInteractiveOnboarding(
|
|||||||
const portRaw = guardCancel(
|
const portRaw = guardCancel(
|
||||||
await text({
|
await text({
|
||||||
message: "Gateway port",
|
message: "Gateway port",
|
||||||
initialValue: "18789",
|
initialValue: String(localPort),
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
Number.isFinite(Number(value)) ? undefined : "Invalid port",
|
||||||
}),
|
}),
|
||||||
@@ -457,6 +459,7 @@ export async function runInteractiveOnboarding(
|
|||||||
...nextConfig,
|
...nextConfig,
|
||||||
gateway: {
|
gateway: {
|
||||||
...nextConfig.gateway,
|
...nextConfig.gateway,
|
||||||
|
port,
|
||||||
bind,
|
bind,
|
||||||
tailscale: {
|
tailscale: {
|
||||||
...nextConfig.gateway?.tailscale,
|
...nextConfig.gateway?.tailscale,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type ClawdisConfig,
|
type ClawdisConfig,
|
||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
|
resolveGatewayPort,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||||
@@ -106,12 +107,18 @@ export async function runNonInteractiveOnboarding(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = opts.gatewayPort ?? 18789;
|
const hasGatewayPort = opts.gatewayPort !== undefined;
|
||||||
if (!Number.isFinite(port) || port <= 0) {
|
if (
|
||||||
|
hasGatewayPort &&
|
||||||
|
(!Number.isFinite(opts.gatewayPort) || (opts.gatewayPort ?? 0) <= 0)
|
||||||
|
) {
|
||||||
runtime.error("Invalid --gateway-port");
|
runtime.error("Invalid --gateway-port");
|
||||||
runtime.exit(1);
|
runtime.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const port = hasGatewayPort
|
||||||
|
? (opts.gatewayPort as number)
|
||||||
|
: resolveGatewayPort(baseConfig);
|
||||||
let bind = opts.gatewayBind ?? "loopback";
|
let bind = opts.gatewayBind ?? "loopback";
|
||||||
let authMode = opts.gatewayAuth ?? "off";
|
let authMode = opts.gatewayAuth ?? "off";
|
||||||
const tailscaleMode = opts.tailscale ?? "off";
|
const tailscaleMode = opts.tailscale ?? "off";
|
||||||
@@ -162,6 +169,7 @@ export async function runNonInteractiveOnboarding(
|
|||||||
...nextConfig,
|
...nextConfig,
|
||||||
gateway: {
|
gateway: {
|
||||||
...nextConfig.gateway,
|
...nextConfig.gateway,
|
||||||
|
port,
|
||||||
bind,
|
bind,
|
||||||
tailscale: {
|
tailscale: {
|
||||||
...nextConfig.gateway?.tailscale,
|
...nextConfig.gateway?.tailscale,
|
||||||
|
|||||||
@@ -390,6 +390,30 @@ describe("Nix integration (U3, U5, U9)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("U6: gateway port resolution", () => {
|
||||||
|
it("uses default when env and config are unset", async () => {
|
||||||
|
await withEnvOverride({ CLAWDIS_GATEWAY_PORT: undefined }, async () => {
|
||||||
|
const { DEFAULT_GATEWAY_PORT, resolveGatewayPort } =
|
||||||
|
await import("./config.js");
|
||||||
|
expect(resolveGatewayPort({})).toBe(DEFAULT_GATEWAY_PORT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers CLAWDIS_GATEWAY_PORT over config", async () => {
|
||||||
|
await withEnvOverride({ CLAWDIS_GATEWAY_PORT: "19001" }, async () => {
|
||||||
|
const { resolveGatewayPort } = await import("./config.js");
|
||||||
|
expect(resolveGatewayPort({ gateway: { port: 19002 } })).toBe(19001);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to config when env is invalid", async () => {
|
||||||
|
await withEnvOverride({ CLAWDIS_GATEWAY_PORT: "nope" }, async () => {
|
||||||
|
const { resolveGatewayPort } = await import("./config.js");
|
||||||
|
expect(resolveGatewayPort({ gateway: { port: 19003 } })).toBe(19003);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("U9: telegram.tokenFile schema validation", () => {
|
describe("U9: telegram.tokenFile schema validation", () => {
|
||||||
it("accepts config with only botToken", async () => {
|
it("accepts config with only botToken", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
|
|||||||
@@ -438,6 +438,8 @@ export type GatewayRemoteConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayConfig = {
|
export type GatewayConfig = {
|
||||||
|
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
|
||||||
|
port?: number;
|
||||||
/**
|
/**
|
||||||
* Explicit gateway mode. When set to "remote", local gateway start is disabled.
|
* Explicit gateway mode. When set to "remote", local gateway start is disabled.
|
||||||
* When set to "local", the CLI may start the gateway locally.
|
* When set to "local", the CLI may start the gateway locally.
|
||||||
@@ -642,6 +644,24 @@ export const CONFIG_PATH_CLAWDIS =
|
|||||||
process.env.CLAWDIS_CONFIG_PATH ??
|
process.env.CLAWDIS_CONFIG_PATH ??
|
||||||
path.join(STATE_DIR_CLAWDIS, "clawdis.json");
|
path.join(STATE_DIR_CLAWDIS, "clawdis.json");
|
||||||
|
|
||||||
|
export const DEFAULT_GATEWAY_PORT = 18789;
|
||||||
|
|
||||||
|
export function resolveGatewayPort(
|
||||||
|
cfg?: ClawdisConfig,
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): number {
|
||||||
|
const envRaw = env.CLAWDIS_GATEWAY_PORT?.trim();
|
||||||
|
if (envRaw) {
|
||||||
|
const parsed = Number.parseInt(envRaw, 10);
|
||||||
|
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
||||||
|
}
|
||||||
|
const configPort = cfg?.gateway?.port;
|
||||||
|
if (typeof configPort === "number" && Number.isFinite(configPort)) {
|
||||||
|
if (configPort > 0) return configPort;
|
||||||
|
}
|
||||||
|
return DEFAULT_GATEWAY_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
const ModelApiSchema = z.union([
|
const ModelApiSchema = z.union([
|
||||||
z.literal("openai-completions"),
|
z.literal("openai-completions"),
|
||||||
z.literal("openai-responses"),
|
z.literal("openai-responses"),
|
||||||
@@ -1217,6 +1237,7 @@ const ClawdisSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
gateway: z
|
gateway: z
|
||||||
.object({
|
.object({
|
||||||
|
port: z.number().int().positive().optional(),
|
||||||
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
||||||
bind: z
|
bind: z
|
||||||
.union([
|
.union([
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
||||||
import { GatewayClient } from "./client.js";
|
import { GatewayClient } from "./client.js";
|
||||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ export async function callGateway<T = unknown>(
|
|||||||
const isRemoteMode = config.gateway?.mode === "remote";
|
const isRemoteMode = config.gateway?.mode === "remote";
|
||||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||||
const authToken = config.gateway?.auth?.token;
|
const authToken = config.gateway?.auth?.token;
|
||||||
|
const localPort = resolveGatewayPort(config);
|
||||||
const url =
|
const url =
|
||||||
(typeof opts.url === "string" && opts.url.trim().length > 0
|
(typeof opts.url === "string" && opts.url.trim().length > 0
|
||||||
? opts.url.trim()
|
? opts.url.trim()
|
||||||
@@ -35,7 +36,7 @@ export async function callGateway<T = unknown>(
|
|||||||
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
||||||
? remote.url.trim()
|
? remote.url.trim()
|
||||||
: undefined) ||
|
: undefined) ||
|
||||||
"ws://127.0.0.1:18789";
|
`ws://127.0.0.1:${localPort}`;
|
||||||
const token =
|
const token =
|
||||||
(typeof opts.token === "string" && opts.token.trim().length > 0
|
(typeof opts.token === "string" && opts.token.trim().length > 0
|
||||||
? opts.token.trim()
|
? opts.token.trim()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
|
resolveGatewayPort,
|
||||||
validateConfigObject,
|
validateConfigObject,
|
||||||
writeConfigFile,
|
writeConfigFile,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
@@ -128,7 +129,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
|
|||||||
const hookUrl =
|
const hookUrl =
|
||||||
opts.hookUrl ??
|
opts.hookUrl ??
|
||||||
baseConfig.hooks?.gmail?.hookUrl ??
|
baseConfig.hooks?.gmail?.hookUrl ??
|
||||||
buildDefaultHookUrl(hooksPath);
|
buildDefaultHookUrl(hooksPath, resolveGatewayPort(baseConfig));
|
||||||
|
|
||||||
const serveBind = opts.bind ?? DEFAULT_GMAIL_SERVE_BIND;
|
const serveBind = opts.bind ?? DEFAULT_GMAIL_SERVE_BIND;
|
||||||
const servePort = opts.port ?? DEFAULT_GMAIL_SERVE_PORT;
|
const servePort = opts.port ?? DEFAULT_GMAIL_SERVE_PORT;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import { type ClawdisConfig, DEFAULT_GATEWAY_PORT } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
buildDefaultHookUrl,
|
buildDefaultHookUrl,
|
||||||
buildTopicPath,
|
buildTopicPath,
|
||||||
@@ -20,8 +20,8 @@ const baseConfig = {
|
|||||||
|
|
||||||
describe("gmail hook config", () => {
|
describe("gmail hook config", () => {
|
||||||
it("builds default hook url", () => {
|
it("builds default hook url", () => {
|
||||||
expect(buildDefaultHookUrl("/hooks")).toBe(
|
expect(buildDefaultHookUrl("/hooks", DEFAULT_GATEWAY_PORT)).toBe(
|
||||||
"http://127.0.0.1:18789/hooks/gmail",
|
`http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/hooks/gmail`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +41,9 @@ describe("gmail hook config", () => {
|
|||||||
expect(result.value.label).toBe("INBOX");
|
expect(result.value.label).toBe("INBOX");
|
||||||
expect(result.value.includeBody).toBe(true);
|
expect(result.value.includeBody).toBe(true);
|
||||||
expect(result.value.serve.port).toBe(8788);
|
expect(result.value.serve.port).toBe(8788);
|
||||||
expect(result.value.hookUrl).toBe("http://127.0.0.1:18789/hooks/gmail");
|
expect(result.value.hookUrl).toBe(
|
||||||
|
`http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/hooks/gmail`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
ClawdisConfig,
|
type ClawdisConfig,
|
||||||
|
DEFAULT_GATEWAY_PORT,
|
||||||
HooksGmailTailscaleMode,
|
HooksGmailTailscaleMode,
|
||||||
|
resolveGatewayPort,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
|
|
||||||
export const DEFAULT_GMAIL_LABEL = "INBOX";
|
export const DEFAULT_GMAIL_LABEL = "INBOX";
|
||||||
@@ -14,7 +16,6 @@ export const DEFAULT_GMAIL_SERVE_PATH = "/gmail-pubsub";
|
|||||||
export const DEFAULT_GMAIL_MAX_BYTES = 20_000;
|
export const DEFAULT_GMAIL_MAX_BYTES = 20_000;
|
||||||
export const DEFAULT_GMAIL_RENEW_MINUTES = 12 * 60;
|
export const DEFAULT_GMAIL_RENEW_MINUTES = 12 * 60;
|
||||||
export const DEFAULT_HOOKS_PATH = "/hooks";
|
export const DEFAULT_HOOKS_PATH = "/hooks";
|
||||||
export const DEFAULT_HOOKS_BASE_URL = "http://127.0.0.1:18789";
|
|
||||||
|
|
||||||
export type GmailHookOverrides = {
|
export type GmailHookOverrides = {
|
||||||
account?: string;
|
account?: string;
|
||||||
@@ -87,9 +88,13 @@ export function normalizeServePath(raw?: string): string {
|
|||||||
return withSlash.replace(/\/+$/, "");
|
return withSlash.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDefaultHookUrl(hooksPath?: string): string {
|
export function buildDefaultHookUrl(
|
||||||
|
hooksPath?: string,
|
||||||
|
port: number = DEFAULT_GATEWAY_PORT,
|
||||||
|
): string {
|
||||||
const basePath = normalizeHooksPath(hooksPath);
|
const basePath = normalizeHooksPath(hooksPath);
|
||||||
return joinUrl(DEFAULT_HOOKS_BASE_URL, `${basePath}/gmail`);
|
const baseUrl = `http://127.0.0.1:${port}`;
|
||||||
|
return joinUrl(baseUrl, `${basePath}/gmail`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGmailHookRuntimeConfig(
|
export function resolveGmailHookRuntimeConfig(
|
||||||
@@ -122,7 +127,9 @@ export function resolveGmailHookRuntimeConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hookUrl =
|
const hookUrl =
|
||||||
overrides.hookUrl ?? gmail?.hookUrl ?? buildDefaultHookUrl(hooks?.path);
|
overrides.hookUrl ??
|
||||||
|
gmail?.hookUrl ??
|
||||||
|
buildDefaultHookUrl(hooks?.path, resolveGatewayPort(cfg));
|
||||||
|
|
||||||
const includeBody = overrides.includeBody ?? gmail?.includeBody ?? true;
|
const includeBody = overrides.includeBody ?? gmail?.includeBody ?? true;
|
||||||
|
|
||||||
|
|||||||
@@ -61,15 +61,18 @@ async function main() {
|
|||||||
wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto";
|
wsLogRaw === "compact" ? "compact" : wsLogRaw === "full" ? "full" : "auto";
|
||||||
setGatewayWsLogStyle(wsLogStyle);
|
setGatewayWsLogStyle(wsLogStyle);
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
const portRaw =
|
const portRaw =
|
||||||
argValue(args, "--port") ?? process.env.CLAWDIS_GATEWAY_PORT ?? "18789";
|
argValue(args, "--port") ??
|
||||||
|
process.env.CLAWDIS_GATEWAY_PORT ??
|
||||||
|
(typeof cfg.gateway?.port === "number" ? String(cfg.gateway.port) : "") ??
|
||||||
|
"18789";
|
||||||
const port = Number.parseInt(portRaw, 10);
|
const port = Number.parseInt(portRaw, 10);
|
||||||
if (Number.isNaN(port) || port <= 0) {
|
if (Number.isNaN(port) || port <= 0) {
|
||||||
defaultRuntime.error(`Invalid --port (${portRaw})`);
|
defaultRuntime.error(`Invalid --port (${portRaw})`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = loadConfig();
|
|
||||||
const bindRaw =
|
const bindRaw =
|
||||||
argValue(args, "--bind") ??
|
argValue(args, "--bind") ??
|
||||||
process.env.CLAWDIS_GATEWAY_BIND ??
|
process.env.CLAWDIS_GATEWAY_BIND ??
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
||||||
import { GatewayClient } from "../gateway/client.js";
|
import { GatewayClient } from "../gateway/client.js";
|
||||||
import {
|
import {
|
||||||
type HelloOk,
|
type HelloOk,
|
||||||
@@ -183,6 +183,7 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) {
|
|||||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||||
const authToken = config.gateway?.auth?.token;
|
const authToken = config.gateway?.auth?.token;
|
||||||
|
|
||||||
|
const localPort = resolveGatewayPort(config);
|
||||||
const url =
|
const url =
|
||||||
(typeof opts.url === "string" && opts.url.trim().length > 0
|
(typeof opts.url === "string" && opts.url.trim().length > 0
|
||||||
? opts.url.trim()
|
? opts.url.trim()
|
||||||
@@ -190,7 +191,7 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) {
|
|||||||
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
||||||
? remote.url.trim()
|
? remote.url.trim()
|
||||||
: undefined) ||
|
: undefined) ||
|
||||||
"ws://127.0.0.1:18789";
|
`ws://127.0.0.1:${localPort}`;
|
||||||
|
|
||||||
const token =
|
const token =
|
||||||
(typeof opts.token === "string" && opts.token.trim().length > 0
|
(typeof opts.token === "string" && opts.token.trim().length > 0
|
||||||
|
|||||||
Reference in New Issue
Block a user