diff --git a/CHANGELOG.md b/CHANGELOG.md index 4429310d8..13ae8295c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - Telegram/WhatsApp: native replies now target the original inbound message; reply context is appended to `Body` and captured in `ReplyTo*` fields. (Thanks @joshp123 for the PR and follow-up question.) - WhatsApp web creds persistence hardened; credentials are restored before auth checks and QR login auto-restarts if it stalls. - Group chats now honor `inbound.groupChat.requireMention=false` as the default activation when no per-group override exists. +- Gateway auth no longer supports PAM/system mode; use token or shared password. +- Tailscale Funnel now requires password auth (no token-only public exposure). - Canvas defaults/A2UI auto-nav aligned; debug status overlay centered; redundant await removed in `CanvasManager`. - Gateway launchd loop fixed by removing redundant `kickstart -k`. - CLI now hints when Peekaboo is unauthorized. diff --git a/apps/macos/Sources/Clawdis/TailscaleIntegrationSection.swift b/apps/macos/Sources/Clawdis/TailscaleIntegrationSection.swift index 574e28521..87e1a211f 100644 --- a/apps/macos/Sources/Clawdis/TailscaleIntegrationSection.swift +++ b/apps/macos/Sources/Clawdis/TailscaleIntegrationSection.swift @@ -27,20 +27,6 @@ private enum GatewayTailscaleMode: String, CaseIterable, Identifiable { } } -private enum GatewayAuthMode: String, CaseIterable, Identifiable { - case system - case password - - var id: String { self.rawValue } - - var label: String { - switch self { - case .system: "System password" - case .password: "Shared password" - } - } -} - struct TailscaleIntegrationSection: View { let connectionMode: AppState.ConnectionMode let isPaused: Bool @@ -50,8 +36,6 @@ struct TailscaleIntegrationSection: View { @State private var hasLoaded = false @State private var tailscaleMode: GatewayTailscaleMode = .off @State private var requireCredentialsForServe = false - @State private var authMode: GatewayAuthMode = .system - @State private var username: String = "" @State private var password: String = "" @State private var statusMessage: String? @State private var validationMessage: String? @@ -115,9 +99,6 @@ struct TailscaleIntegrationSection: View { .onChange(of: self.requireCredentialsForServe) { _, _ in self.applySettings() } - .onChange(of: self.authMode) { _, _ in - self.applySettings() - } } private var statusRow: some View { @@ -210,7 +191,6 @@ struct TailscaleIntegrationSection: View { Toggle("Require credentials", isOn: self.$requireCredentialsForServe) .toggleStyle(.checkbox) if self.requireCredentialsForServe { - self.authModePicker self.authFields } else { Text("Serve uses Tailscale identity headers; no password required.") @@ -225,39 +205,22 @@ struct TailscaleIntegrationSection: View { Text("Funnel requires authentication.") .font(.caption) .foregroundStyle(.secondary) - self.authModePicker self.authFields } } - private var authModePicker: some View { - Picker("Auth", selection: self.$authMode) { - ForEach(GatewayAuthMode.allCases) { mode in - Text(mode.label).tag(mode) - } - } - .pickerStyle(.segmented) - } - @ViewBuilder private var authFields: some View { - if self.authMode == .system { - TextField("Username (optional)", text: self.$username) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 240) - .onSubmit { self.applySettings() } - } else { - SecureField("Password", text: self.$password) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 240) - .onSubmit { self.applySettings() } - Text("Stored in ~/.clawdis/clawdis.json. Prefer CLAWDIS_GATEWAY_PASSWORD for production.") - .font(.caption) - .foregroundStyle(.secondary) - Button("Update password") { self.applySettings() } - .buttonStyle(.bordered) - .controlSize(.small) - } + SecureField("Password", text: self.$password) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 240) + .onSubmit { self.applySettings() } + Text("Stored in ~/.clawdis/clawdis.json. Prefer CLAWDIS_GATEWAY_PASSWORD for production.") + .font(.caption) + .foregroundStyle(.secondary) + Button("Update password") { self.applySettings() } + .buttonStyle(.bordered) + .controlSize(.small) } private func loadConfig() { @@ -270,17 +233,10 @@ struct TailscaleIntegrationSection: View { let authModeRaw = auth["mode"] as? String let allowTailscale = auth["allowTailscale"] as? Bool - if let authModeRaw, authModeRaw == "password" { - self.authMode = .password - } else { - self.authMode = .system - } - - self.username = auth["username"] as? String ?? "" self.password = auth["password"] as? String ?? "" if self.tailscaleMode == .serve { - let usesExplicitAuth = authModeRaw == "password" || authModeRaw == "system" + let usesExplicitAuth = authModeRaw == "password" if let allowTailscale, allowTailscale == false { self.requireCredentialsForServe = true } else { @@ -299,7 +255,7 @@ struct TailscaleIntegrationSection: View { let trimmedPassword = self.password.trimmingCharacters(in: .whitespacesAndNewlines) let requiresPassword = self.tailscaleMode == .funnel || (self.tailscaleMode == .serve && self.requireCredentialsForServe) - if requiresPassword, self.authMode == .password, trimmedPassword.isEmpty { + if requiresPassword, trimmedPassword.isEmpty { self.validationMessage = "Password required for this mode." return } @@ -320,22 +276,10 @@ struct TailscaleIntegrationSection: View { auth["allowTailscale"] = true auth.removeValue(forKey: "mode") auth.removeValue(forKey: "password") - auth.removeValue(forKey: "username") } else { auth["allowTailscale"] = false - auth["mode"] = self.authMode.rawValue - if self.authMode == .password { - auth["password"] = trimmedPassword - auth.removeValue(forKey: "username") - } else { - let trimmedUsername = self.username.trimmingCharacters(in: .whitespacesAndNewlines) - if trimmedUsername.isEmpty { - auth.removeValue(forKey: "username") - } else { - auth["username"] = trimmedUsername - } - auth.removeValue(forKey: "password") - } + auth["mode"] = "password" + auth["password"] = trimmedPassword } if auth.isEmpty { diff --git a/docs/configuration.md b/docs/configuration.md index f4d734977..19cad5186 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -281,7 +281,7 @@ Defaults: mode: "local", // or "remote" bind: "loopback", // controlUi: { enabled: true } - // auth: { mode: "token" | "password" | "system" } + // auth: { mode: "token" | "password" } // tailscale: { mode: "off" | "serve" | "funnel" } } } @@ -291,10 +291,9 @@ Notes: - `clawdis gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag). Auth and Tailscale: -- `gateway.auth.mode` sets the handshake requirements (`token`, `password`, or `system`/PAM). +- `gateway.auth.mode` sets the handshake requirements (`token` or `password`). - When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers). - `gateway.auth.password` can be set here, or via `CLAWDIS_GATEWAY_PASSWORD` (recommended). -- `gateway.auth.username` defaults to the current OS user; override with `CLAWDIS_GATEWAY_USERNAME`. - `gateway.auth.allowTailscale` controls whether Tailscale identity headers can satisfy auth. - `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind). - `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth. diff --git a/docs/control-ui.md b/docs/control-ui.md index 978573857..01f6e3139 100644 --- a/docs/control-ui.md +++ b/docs/control-ui.md @@ -14,8 +14,8 @@ It speaks **directly to the Gateway WebSocket** on the same port. Auth is supplied during the WebSocket handshake via: - `connect.params.auth.token` -- `connect.params.auth.password` (optional `username` for system/PAM) -The dashboard settings panel lets you store a token and optional username; passwords are not persisted. +- `connect.params.auth.password` +The dashboard settings panel lets you store a token; passwords are not persisted. ## What it can do (today) - Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`) diff --git a/docs/tailscale.md b/docs/tailscale.md index 4d0360d1d..03408890f 100644 --- a/docs/tailscale.md +++ b/docs/tailscale.md @@ -13,7 +13,7 @@ Tailscale provides HTTPS, routing, and (for Serve) identity headers. ## Modes - `serve`: Tailnet-only HTTPS via `tailscale serve`. The gateway stays on `127.0.0.1`. -- `funnel`: Public HTTPS via `tailscale funnel`. Requires auth. +- `funnel`: Public HTTPS via `tailscale funnel`. Requires a shared password. - `off`: Default (no Tailscale automation). ## Auth @@ -22,10 +22,9 @@ Set `gateway.auth.mode` to control the handshake: - `token` (default when `CLAWDIS_GATEWAY_TOKEN` is set) - `password` (shared secret via `CLAWDIS_GATEWAY_PASSWORD` or config) -- `system` (PAM, validates your OS password) When `tailscale.mode = "serve"`, the gateway trusts Tailscale identity headers by -default unless you force `gateway.auth.mode` to `password`/`system` or set +default unless you force `gateway.auth.mode` to `password` or set `gateway.auth.allowTailscale: false`. ## Config examples @@ -43,20 +42,6 @@ default unless you force `gateway.auth.mode` to `password`/`system` or set Open: `https:///ui/` -### Public internet (Funnel + system password) - -```json5 -{ - gateway: { - bind: "loopback", - tailscale: { mode: "funnel" }, - auth: { mode: "system" } - } -} -``` - -Open: `https:///ui/` (public) - ### Public internet (Funnel + shared password) ```json5 @@ -75,13 +60,12 @@ Prefer `CLAWDIS_GATEWAY_PASSWORD` over committing a password to disk. ```bash clawdis gateway --tailscale serve -clawdis gateway --tailscale funnel --auth system +clawdis gateway --tailscale funnel --auth password ``` ## Notes - Tailscale Serve/Funnel requires the `tailscale` CLI to be installed and logged in. -- System auth uses the optional `authenticate-pam` native module; install if missing. -- `tailscale.mode: "funnel"` refuses to start without auth to avoid public exposure. +- `tailscale.mode: "funnel"` refuses to start unless auth mode is `password` to avoid public exposure. - Set `gateway.tailscale.resetOnExit` if you want Clawdis to undo `tailscale serve` or `tailscale funnel` configuration on shutdown. diff --git a/docs/web.md b/docs/web.md index 7555d60a5..1cd7bbdf9 100644 --- a/docs/web.md +++ b/docs/web.md @@ -86,7 +86,7 @@ Open: gateway: { bind: "loopback", tailscale: { mode: "funnel" }, - auth: { mode: "system" } // or "password" with CLAWDIS_GATEWAY_PASSWORD + auth: { mode: "password" } // or CLAWDIS_GATEWAY_PASSWORD } } ``` @@ -94,9 +94,9 @@ Open: ## Security notes - Binding the Gateway to a non-loopback address **requires** auth (`CLAWDIS_GATEWAY_TOKEN` or `gateway.auth`). -- `gateway.auth.mode: "system"` uses PAM to verify your OS password. - The UI sends `connect.params.auth.token` or `connect.params.auth.password`. - Use `gateway.auth.allowTailscale: false` to require explicit credentials even in Serve mode. +- `gateway.tailscale.mode: "funnel"` requires `gateway.auth.mode: "password"` (shared password). ## Building the UI diff --git a/package.json b/package.json index 80ccde216..a0aa33395 100644 --- a/package.json +++ b/package.json @@ -94,9 +94,6 @@ "ws": "^8.18.3", "zod": "^4.2.1" }, - "optionalDependencies": { - "authenticate-pam": "^1.0.5" - }, "devDependencies": { "@biomejs/biome": "^2.3.10", "@lit-labs/signals": "^0.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5393138b2..d76b3da10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,10 +171,6 @@ importers: wireit: specifier: ^0.14.12 version: 0.14.12 - optionalDependencies: - authenticate-pam: - specifier: ^1.0.5 - version: 1.0.5 packages: @@ -1242,9 +1238,6 @@ packages: resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==} engines: {node: '>=14'} - authenticate-pam@1.0.5: - resolution: {integrity: sha512-zaPml3/19Sa3XLewuOoUNsxwnNz13mTNoO4Q09vr93cjTrH0dwXOU49Bcetk/XWl22bw9zO9WovSKkddGvBEsQ==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1990,9 +1983,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.24.0: - resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3629,11 +3619,6 @@ snapshots: audio-type@2.2.1: optional: true - authenticate-pam@1.0.5: - dependencies: - nan: 2.24.0 - optional: true - balanced-match@1.0.2: {} balanced-match@3.0.1: {} @@ -4421,9 +4406,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.24.0: - optional: true - nanoid@3.3.11: {} negotiator@1.0.0: {} diff --git a/src/cli/gateway-cli.ts b/src/cli/gateway-cli.ts index 68ece3514..879137255 100644 --- a/src/cli/gateway-cli.ts +++ b/src/cli/gateway-cli.ts @@ -18,7 +18,6 @@ import { forceFreePortAndWait } from "./ports.js"; type GatewayRpcOpts = { url?: string; token?: string; - username?: string; password?: string; timeout?: string; expectFinal?: boolean; @@ -30,8 +29,7 @@ const gatewayCallOpts = (cmd: Command) => cmd .option("--url ", "Gateway WebSocket URL", "ws://127.0.0.1:18789") .option("--token ", "Gateway token (if required)") - .option("--username ", "Gateway username (system auth)") - .option("--password ", "Gateway password (password/system auth)") + .option("--password ", "Gateway password (password auth)") .option("--timeout ", "Timeout in ms", "10000") .option("--expect-final", "Wait for final response (agent)", false); @@ -43,7 +41,6 @@ const callGatewayCli = async ( callGateway({ url: opts.url, token: opts.token, - username: opts.username, password: opts.password, method, params, @@ -66,9 +63,8 @@ export function registerGatewayCli(program: Command) { "--token ", "Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)", ) - .option("--auth ", 'Gateway auth mode ("token"|"password"|"system")') + .option("--auth ", 'Gateway auth mode ("token"|"password")') .option("--password ", "Password for auth mode=password") - .option("--username ", "Default username for system auth") .option( "--tailscale ", 'Tailscale exposure mode ("off"|"serve"|"funnel")', @@ -121,13 +117,12 @@ export function registerGatewayCli(program: Command) { const authModeRaw = opts.auth ? String(opts.auth) : undefined; const authMode = authModeRaw === "token" || - authModeRaw === "password" || - authModeRaw === "system" + authModeRaw === "password" ? authModeRaw : null; if (authModeRaw && !authMode) { defaultRuntime.error( - 'Invalid --auth (use "token", "password", or "system")', + 'Invalid --auth (use "token" or "password")', ); defaultRuntime.exit(1); return; @@ -205,11 +200,10 @@ export function registerGatewayCli(program: Command) { server = await startGatewayServer(port, { bind, auth: - authMode || opts.password || opts.username || authModeRaw + authMode || opts.password || authModeRaw ? { mode: authMode ?? undefined, password: opts.password ? String(opts.password) : undefined, - username: opts.username ? String(opts.username) : undefined, } : undefined, tailscale: @@ -245,9 +239,8 @@ export function registerGatewayCli(program: Command) { "--token ", "Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)", ) - .option("--auth ", 'Gateway auth mode ("token"|"password"|"system")') + .option("--auth ", 'Gateway auth mode ("token"|"password")') .option("--password ", "Password for auth mode=password") - .option("--username ", "Default username for system auth") .option( "--tailscale ", 'Tailscale exposure mode ("off"|"serve"|"funnel")', @@ -342,13 +335,12 @@ export function registerGatewayCli(program: Command) { const authModeRaw = opts.auth ? String(opts.auth) : undefined; const authMode = authModeRaw === "token" || - authModeRaw === "password" || - authModeRaw === "system" + authModeRaw === "password" ? authModeRaw : null; if (authModeRaw && !authMode) { defaultRuntime.error( - 'Invalid --auth (use "token", "password", or "system")', + 'Invalid --auth (use "token" or "password")', ); defaultRuntime.exit(1); return; @@ -443,11 +435,10 @@ export function registerGatewayCli(program: Command) { server = await startGatewayServer(port, { bind, auth: - authMode || opts.password || opts.username || authModeRaw + authMode || opts.password || authModeRaw ? { mode: authMode ?? undefined, password: opts.password ? String(opts.password) : undefined, - username: opts.username ? String(opts.username) : undefined, } : undefined, tailscale: diff --git a/src/config/config.ts b/src/config/config.ts index 4e6854c5b..3ca63477a 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -115,13 +115,11 @@ export type GatewayControlUiConfig = { enabled?: boolean; }; -export type GatewayAuthMode = "token" | "password" | "system"; +export type GatewayAuthMode = "token" | "password"; export type GatewayAuthConfig = { /** Authentication mode for Gateway connections. Defaults to token when set. */ mode?: GatewayAuthMode; - /** Username for system auth (PAM). Defaults to current user. */ - username?: string; /** Shared password for password mode (consider env instead). */ password?: string; /** Allow Tailscale identity headers when serve mode is enabled. */ @@ -522,10 +520,8 @@ const ClawdisSchema = z.object({ .union([ z.literal("token"), z.literal("password"), - z.literal("system"), ]) .optional(), - username: z.string().optional(), password: z.string().optional(), allowTailscale: z.boolean().optional(), }) diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index b1e543200..cb179e7d5 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -1,29 +1,23 @@ import { timingSafeEqual } from "node:crypto"; import type { IncomingMessage } from "node:http"; -import os from "node:os"; - -import { type PamAvailability, verifyPamCredentials } from "../infra/pam.js"; - -export type ResolvedGatewayAuthMode = "none" | "token" | "password" | "system"; +export type ResolvedGatewayAuthMode = "none" | "token" | "password"; export type ResolvedGatewayAuth = { mode: ResolvedGatewayAuthMode; token?: string; password?: string; - username?: string; allowTailscale: boolean; }; export type GatewayAuthResult = { ok: boolean; - method?: "none" | "token" | "password" | "system" | "tailscale"; + method?: "none" | "token" | "password" | "tailscale"; user?: string; reason?: string; }; type ConnectAuth = { token?: string; - username?: string; password?: string; }; @@ -104,10 +98,7 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean { ); } -export function assertGatewayAuthConfigured( - auth: ResolvedGatewayAuth, - pam: PamAvailability, -): void { +export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void { if (auth.mode === "token" && !auth.token) { throw new Error( "gateway auth mode is token, but CLAWDIS_GATEWAY_TOKEN is not set", @@ -118,13 +109,6 @@ export function assertGatewayAuthConfigured( "gateway auth mode is password, but no password was configured", ); } - if (auth.mode === "system" && !pam.available) { - throw new Error( - `gateway auth mode is system, but PAM auth is unavailable${ - pam.error ? `: ${pam.error}` : "" - }`, - ); - } } export async function authorizeGatewayConnect(params: { @@ -170,21 +154,6 @@ export async function authorizeGatewayConnect(params: { return { ok: true, method: "password" }; } - if (auth.mode === "system") { - const password = connectAuth?.password; - if (!password) return { ok: false, reason: "unauthorized" }; - const username = ( - connectAuth?.username ?? - auth.username ?? - os.userInfo().username - ).trim(); - if (!username) return { ok: false, reason: "unauthorized" }; - const ok = await verifyPamCredentials(username, password); - return ok - ? { ok: true, method: "system", user: username } - : { ok: false, reason: "unauthorized" }; - } - if (auth.allowTailscale) { const tailscaleUser = getTailscaleUser(req); if (tailscaleUser && isTailscaleProxyRequest(req)) { diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 032a07404..3ebdcd0dd 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -5,7 +5,6 @@ import { PROTOCOL_VERSION } from "./protocol/index.js"; export type CallGatewayOptions = { url?: string; token?: string; - username?: string; password?: string; method: string; params?: unknown; @@ -37,7 +36,6 @@ export async function callGateway( const client = new GatewayClient({ url: opts.url, token: opts.token, - username: opts.username, password: opts.password, instanceId: opts.instanceId ?? randomUUID(), clientName: opts.clientName ?? "cli", diff --git a/src/gateway/client.ts b/src/gateway/client.ts index 1dc5a0215..a29fa284c 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -20,7 +20,6 @@ type Pending = { export type GatewayClientOptions = { url?: string; // ws://127.0.0.1:18789 token?: string; - username?: string; password?: string; instanceId?: string; clientName?: string; @@ -86,10 +85,9 @@ export class GatewayClient { private sendConnect() { const auth = - this.opts.token || this.opts.password || this.opts.username + this.opts.token || this.opts.password ? { token: this.opts.token, - username: this.opts.username, password: this.opts.password, } : undefined; diff --git a/src/gateway/protocol/schema.ts b/src/gateway/protocol/schema.ts index 6445b4fc5..7e581bbad 100644 --- a/src/gateway/protocol/schema.ts +++ b/src/gateway/protocol/schema.ts @@ -77,7 +77,6 @@ export const ConnectParamsSchema = Type.Object( Type.Object( { token: Type.Optional(Type.String()), - username: Type.Optional(Type.String()), password: Type.Optional(Type.String()), }, { additionalProperties: false }, diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 0f87844b2..ab01d92c7 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -338,7 +338,6 @@ async function connectReq( ws: WebSocket, opts?: { token?: string; - username?: string; password?: string; minProtocol?: number; maxProtocol?: number; @@ -368,10 +367,9 @@ async function connectReq( }, caps: [], auth: - opts?.token || opts?.password || opts?.username + opts?.token || opts?.password ? { token: opts?.token, - username: opts?.username, password: opts?.password, } : undefined, diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 71f6570f0..05e62696d 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -76,7 +76,6 @@ import { requestNodePairing, verifyNodeToken, } from "../infra/node-pairing.js"; -import { getPamAvailability } from "../infra/pam.js"; import { ensureClawdisCliOnPath } from "../infra/path-env.js"; import { enqueueSystemEvent, @@ -1211,30 +1210,24 @@ export async function startGatewayServer( const token = getGatewayToken(); const password = authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined; - const username = - authConfig.username ?? process.env.CLAWDIS_GATEWAY_USERNAME ?? undefined; const authMode: ResolvedGatewayAuth["mode"] = authConfig.mode ?? (password ? "password" : token ? "token" : "none"); const allowTailscale = authConfig.allowTailscale ?? - (tailscaleMode === "serve" && - authMode !== "password" && - authMode !== "system"); + (tailscaleMode === "serve" && authMode !== "password"); const resolvedAuth: ResolvedGatewayAuth = { mode: authMode, token, password, - username, allowTailscale, }; const canvasHostEnabled = process.env.CLAWDIS_SKIP_CANVAS_HOST !== "1" && cfgAtStart.canvasHost?.enabled !== false; - const pamAvailability = await getPamAvailability(); - assertGatewayAuthConfigured(resolvedAuth, pamAvailability); - if (tailscaleMode === "funnel" && authMode === "none") { + assertGatewayAuthConfigured(resolvedAuth); + if (tailscaleMode === "funnel" && authMode !== "password") { throw new Error( - "tailscale funnel requires gateway auth (set gateway.auth or CLAWDIS_GATEWAY_TOKEN)", + "tailscale funnel requires gateway auth mode=password (set gateway.auth.password or CLAWDIS_GATEWAY_PASSWORD)", ); } if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) { diff --git a/src/infra/pam.ts b/src/infra/pam.ts deleted file mode 100644 index 2ed607798..000000000 --- a/src/infra/pam.ts +++ /dev/null @@ -1,61 +0,0 @@ -type PamAuthenticate = ( - username: string, - password: string, - callback: (err: Error | null) => void, -) => void; - -let pamAuth: PamAuthenticate | null | undefined; -let pamError: string | null = null; - -async function loadPam(): Promise { - if (pamAuth !== undefined) return; - try { - // Vite/Vitest: avoid static analysis/bundling for optional native deps. - const pkgName = "authenticate-pam"; - const mod = (await import(pkgName)) as - | { authenticate?: PamAuthenticate; default?: PamAuthenticate } - | PamAuthenticate; - const candidate = - typeof mod === "function" - ? mod - : typeof (mod as { authenticate?: PamAuthenticate }).authenticate === - "function" - ? (mod as { authenticate: PamAuthenticate }).authenticate - : typeof (mod as { default?: PamAuthenticate }).default === "function" - ? (mod as { default: PamAuthenticate }).default - : null; - if (!candidate) { - throw new Error( - "authenticate-pam did not export an authenticate function", - ); - } - pamAuth = candidate; - } catch (err) { - pamAuth = null; - pamError = err instanceof Error ? err.message : String(err); - } -} - -export type PamAvailability = { - available: boolean; - error?: string; -}; - -export async function getPamAvailability(): Promise { - await loadPam(); - return pamAuth - ? { available: true } - : { available: false, error: pamError ?? undefined }; -} - -export async function verifyPamCredentials( - username: string, - password: string, -): Promise { - await loadPam(); - const auth = pamAuth; - if (!auth) return false; - return await new Promise((resolve) => { - auth(username, password, (err) => resolve(!err)); - }); -} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index e761a50f3..6558a70e8 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -48,7 +48,7 @@ export type EventLogEntry = { }; export type AppViewState = { - settings: { gatewayUrl: string; token: string; username: string; sessionKey: string }; + settings: { gatewayUrl: string; token: string; sessionKey: string }; password: string; tab: Tab; connected: boolean; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index c7d0effcb..4eacbb840 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -188,9 +188,6 @@ export class ClawdisApp extends LitElement { this.client = new GatewayBrowserClient({ url: this.settings.gatewayUrl, token: this.settings.token.trim() ? this.settings.token : undefined, - username: this.settings.username.trim() - ? this.settings.username.trim() - : undefined, password: this.password.trim() ? this.password : undefined, clientName: "clawdis-control-ui", mode: "webchat", diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index b536cc2f7..7fd488498 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -30,7 +30,6 @@ type Pending = { export type GatewayBrowserClientOptions = { url: string; token?: string; - username?: string; password?: string; clientName?: string; clientVersion?: string; @@ -99,10 +98,9 @@ export class GatewayBrowserClient { private sendConnect() { const auth = - this.opts.token || this.opts.password || this.opts.username + this.opts.token || this.opts.password ? { token: this.opts.token, - username: this.opts.username, password: this.opts.password, } : undefined; diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index 224e0dcb8..304988e6a 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -3,7 +3,6 @@ const KEY = "clawdis.control.settings.v1"; export type UiSettings = { gatewayUrl: string; token: string; - username: string; sessionKey: string; }; @@ -16,7 +15,6 @@ export function loadSettings(): UiSettings { const defaults: UiSettings = { gatewayUrl: defaultUrl, token: "", - username: "", sessionKey: "main", }; @@ -30,8 +28,6 @@ export function loadSettings(): UiSettings { ? parsed.gatewayUrl.trim() : defaults.gatewayUrl, token: typeof parsed.token === "string" ? parsed.token : defaults.token, - username: - typeof parsed.username === "string" ? parsed.username : defaults.username, sessionKey: typeof parsed.sessionKey === "string" && parsed.sessionKey.trim() ? parsed.sessionKey.trim() diff --git a/ui/src/ui/views/overview.ts b/ui/src/ui/views/overview.ts index 5f4080a33..adb1358ed 100644 --- a/ui/src/ui/views/overview.ts +++ b/ui/src/ui/views/overview.ts @@ -59,17 +59,6 @@ export function renderOverview(props: OverviewProps) { placeholder="CLAWDIS_GATEWAY_TOKEN" /> -