chore: fix lint and add gateway auth tests

This commit is contained in:
Peter Steinberger
2026-01-02 16:56:27 +01:00
parent 8d925226cb
commit 314164fb8a
10 changed files with 115 additions and 36 deletions

View File

@@ -26,13 +26,28 @@ actor GatewayEndpointStore {
mode: { await MainActor.run { AppStateStore.shared.connectionMode } }, mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] }, token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] },
password: { password: {
let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? "" let root = ClawdisConfigFile.loadDict()
return GatewayEndpointStore.resolveGatewayPassword(
isRemote: CommandResolver.connectionModeIsRemote(),
root: root,
env: ProcessInfo.processInfo.environment)
},
localPort: { GatewayEnvironment.gatewayPort() },
remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() },
ensureRemoteTunnel: { try await RemoteTunnelManager.shared.ensureControlTunnel() })
}
private static func resolveGatewayPassword(
isRemote: Bool,
root: [String: Any],
env: [String: String]
) -> String? {
let raw = env["CLAWDIS_GATEWAY_PASSWORD"] ?? ""
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { if !trimmed.isEmpty {
return trimmed return trimmed
} }
let root = ClawdisConfigFile.loadDict() if isRemote {
if CommandResolver.connectionModeIsRemote() {
if let gateway = root["gateway"] as? [String: Any], if let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any], let remote = gateway["remote"] as? [String: Any],
let password = remote["password"] as? String let password = remote["password"] as? String
@@ -54,10 +69,6 @@ actor GatewayEndpointStore {
} }
} }
return nil return nil
},
localPort: { GatewayEnvironment.gatewayPort() },
remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() },
ensureRemoteTunnel: { try await RemoteTunnelManager.shared.ensureControlTunnel() })
} }
private let deps: Deps private let deps: Deps
@@ -193,3 +204,15 @@ actor GatewayEndpointStore {
} }
} }
} }
#if DEBUG
extension GatewayEndpointStore {
static func _testResolveGatewayPassword(
isRemote: Bool,
root: [String: Any],
env: [String: String]
) -> String? {
self.resolveGatewayPassword(isRemote: isRemote, root: root, env: env)
}
}
#endif

View File

@@ -226,5 +226,9 @@ extension GatewayLaunchAgentManager {
static func _testPreferredGatewayToken() -> String? { static func _testPreferredGatewayToken() -> String? {
self.preferredGatewayToken() self.preferredGatewayToken()
} }
static func _testEscapePlistValue(_ raw: String) -> String {
self.escapePlistValue(raw)
}
} }
#endif #endif

View File

@@ -82,6 +82,44 @@ import Testing
#expect(password == "pw") #expect(password == "pw")
} }
@Test func resolvesGatewayPasswordByMode() {
let root: [String: Any] = [
"gateway": [
"auth": ["password": " local "],
"remote": ["password": " remote "],
],
]
let env: [String: String] = [:]
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: false,
root: root,
env: env) == "local")
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: true,
root: root,
env: env) == "remote")
}
@Test func gatewayPasswordEnvOverridesConfig() {
let root: [String: Any] = [
"gateway": [
"auth": ["password": "local"],
"remote": ["password": "remote"],
],
]
let env = ["CLAWDIS_GATEWAY_PASSWORD": " env "]
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: false,
root: root,
env: env) == "env")
#expect(GatewayEndpointStore._testResolveGatewayPassword(
isRemote: true,
root: root,
env: env) == "env")
}
@Test func unconfiguredModeRejectsConfig() async { @Test func unconfiguredModeRejectsConfig() async {
let mode = ModeBox(.unconfigured) let mode = ModeBox(.unconfigured)
let store = GatewayEndpointStore(deps: .init( let store = GatewayEndpointStore(deps: .init(

View File

@@ -114,6 +114,9 @@ struct LowCoverageHelperTests {
setenv(keyToken, " secret ", 1) setenv(keyToken, " secret ", 1)
#expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan") #expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan")
#expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret") #expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret")
#expect(
GatewayLaunchAgentManager._testEscapePlistValue("a&b<c>\"'") ==
"a&amp;b&lt;c&gt;&quot;&apos;")
#expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdis") #expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdis")
#expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay") #expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay")

View File

@@ -854,7 +854,9 @@ export async function getReplyFromConfig(
const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
const defaultAllowFrom = const defaultAllowFrom =
isWhatsAppSurface && (!configuredAllowFrom || configuredAllowFrom.length === 0) && to isWhatsAppSurface &&
(!configuredAllowFrom || configuredAllowFrom.length === 0) &&
to
? [to] ? [to]
: undefined; : undefined;
const allowFrom = const allowFrom =

View File

@@ -10,11 +10,11 @@ import { sessionsCommand } from "../commands/sessions.js";
import { setupCommand } from "../commands/setup.js"; import { setupCommand } from "../commands/setup.js";
import { statusCommand } from "../commands/status.js"; import { statusCommand } from "../commands/status.js";
import { updateCommand } from "../commands/update.js"; import { updateCommand } from "../commands/update.js";
import { readConfigFileSnapshot } from "../config/config.js";
import { danger, setVerbose } from "../globals.js"; import { danger, setVerbose } from "../globals.js";
import { loginWeb, logoutWeb } from "../provider-web.js"; import { loginWeb, logoutWeb } from "../provider-web.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
import { readConfigFileSnapshot } from "../config/config.js";
import { registerBrowserCli } from "./browser-cli.js"; import { registerBrowserCli } from "./browser-cli.js";
import { registerCanvasCli } from "./canvas-cli.js"; import { registerCanvasCli } from "./canvas-cli.js";
import { registerCronCli } from "./cron-cli.js"; import { registerCronCli } from "./cron-cli.js";
@@ -79,7 +79,7 @@ export function buildProgram() {
.join("\n"); .join("\n");
defaultRuntime.error( defaultRuntime.error(
danger( danger(
`Legacy config entries detected. Run \"clawdis doctor\" (or ask your agent) to migrate.\n${issues}`, `Legacy config entries detected. Run "clawdis doctor" (or ask your agent) to migrate.\n${issues}`,
), ),
); );
process.exit(1); process.exit(1);

View File

@@ -30,7 +30,11 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) {
const snapshot = await readConfigFileSnapshot(); const snapshot = await readConfigFileSnapshot();
let cfg: ClawdisConfig = snapshot.valid ? snapshot.config : {}; let cfg: ClawdisConfig = snapshot.valid ? snapshot.config : {};
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) { if (
snapshot.exists &&
!snapshot.valid &&
snapshot.legacyIssues.length === 0
) {
note("Config invalid; doctor will run with defaults.", "Config"); note("Config invalid; doctor will run with defaults.", "Config");
} }
@@ -50,7 +54,9 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) {
); );
if (migrate) { if (migrate) {
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom. // Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed); const { config: migrated, changes } = migrateLegacyConfig(
snapshot.parsed,
);
if (changes.length > 0) { if (changes.length > 0) {
note(changes.join("\n"), "Doctor changes"); note(changes.join("\n"), "Doctor changes");
} }

View File

@@ -508,7 +508,9 @@ describe("legacy config detection", () => {
const res = migrateLegacyConfig({ const res = migrateLegacyConfig({
routing: { allowFrom: ["+15555550123"] }, routing: { allowFrom: ["+15555550123"] },
}); });
expect(res.changes).toContain("Moved routing.allowFrom → whatsapp.allowFrom."); expect(res.changes).toContain(
"Moved routing.allowFrom → whatsapp.allowFrom.",
);
expect(res.config?.whatsapp?.allowFrom).toEqual(["+15555550123"]); expect(res.config?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
expect(res.config?.routing?.allowFrom).toBeUndefined(); expect(res.config?.routing?.allowFrom).toBeUndefined();
}); });

View File

@@ -1217,7 +1217,9 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
whatsapp.allowFrom = allowFrom; whatsapp.allowFrom = allowFrom;
changes.push("Moved routing.allowFrom → whatsapp.allowFrom."); changes.push("Moved routing.allowFrom → whatsapp.allowFrom.");
} else { } else {
changes.push("Removed routing.allowFrom (whatsapp.allowFrom already set)."); changes.push(
"Removed routing.allowFrom (whatsapp.allowFrom already set).",
);
} }
delete (routing as Record<string, unknown>).allowFrom; delete (routing as Record<string, unknown>).allowFrom;

View File

@@ -660,7 +660,6 @@ type DedupeEntry = {
error?: ErrorShape; error?: ErrorShape;
}; };
function formatForLog(value: unknown): string { function formatForLog(value: unknown): string {
try { try {
if (value instanceof Error) { if (value instanceof Error) {
@@ -1334,7 +1333,7 @@ export async function startGatewayServer(
); );
if (!migrated) { if (!migrated) {
throw new Error( throw new Error(
"Legacy config entries detected but auto-migration failed. Run \"clawdis doctor\" to migrate.", 'Legacy config entries detected but auto-migration failed. Run "clawdis doctor" to migrate.',
); );
} }
await writeConfigFile(migrated); await writeConfigFile(migrated);