fix: persist gateway token for local CLI auth
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
|
||||
### Fixes
|
||||
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
||||
- CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.
|
||||
- Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).
|
||||
- Control UI: refine Web Chat session selector styling (chevron spacing + background).
|
||||
- WebChat: stream live updates for sessions even when runs start outside the chat UI.
|
||||
|
||||
@@ -555,7 +555,7 @@ Defaults:
|
||||
mode: "local", // or "remote"
|
||||
bind: "loopback",
|
||||
// controlUi: { enabled: true }
|
||||
// auth: { mode: "token" | "password" }
|
||||
// auth: { mode: "token", token: "your-token" } // token is for multi-machine CLI access
|
||||
// tailscale: { mode: "off" | "serve" | "funnel" }
|
||||
}
|
||||
}
|
||||
@@ -566,6 +566,7 @@ Notes:
|
||||
|
||||
Auth and Tailscale:
|
||||
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
||||
- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine).
|
||||
- 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.allowTailscale` controls whether Tailscale identity headers can satisfy auth.
|
||||
|
||||
@@ -22,6 +22,10 @@ First question: where does the **Gateway** run?
|
||||
- **Local (this Mac):** onboarding can run the Anthropic OAuth flow and write the Clawdis token store locally.
|
||||
- **Remote (over SSH/tailnet):** onboarding must not run OAuth locally, because credentials must exist on the **gateway host**.
|
||||
|
||||
Gateway auth tip:
|
||||
- If you only use Clawdis on this Mac (loopback gateway), keep auth **Off**.
|
||||
- Use **Token** for multi-machine access or non-loopback binds.
|
||||
|
||||
Implementation note (2025-12-19): in local mode, the macOS app bundles the Gateway and enables it via a per-user launchd LaunchAgent (no global npm install/Node requirement for the user).
|
||||
|
||||
## 2) Local-only: Connect Claude (Anthropic OAuth)
|
||||
|
||||
@@ -58,6 +58,7 @@ It does **not** install or change anything on the remote host.
|
||||
|
||||
4) **Gateway**
|
||||
- Port, bind, auth mode, tailscale exposure.
|
||||
- Auth recommendation: keep **Off** for single-machine loopback setups. Use **Token** for multi-machine access or non-loopback binds.
|
||||
- Non‑loopback binds require auth.
|
||||
|
||||
5) **Providers**
|
||||
|
||||
@@ -280,8 +280,16 @@ export async function runInteractiveOnboarding(
|
||||
await select({
|
||||
message: "Gateway auth",
|
||||
options: [
|
||||
{ value: "off", label: "Off (loopback only)" },
|
||||
{ value: "token", label: "Token" },
|
||||
{
|
||||
value: "off",
|
||||
label: "Off (loopback only)",
|
||||
hint: "Recommended for single-machine setups",
|
||||
},
|
||||
{
|
||||
value: "token",
|
||||
label: "Token",
|
||||
hint: "Use for multi-machine access or non-loopback binds",
|
||||
},
|
||||
{ value: "password", label: "Password" },
|
||||
],
|
||||
}),
|
||||
@@ -344,6 +352,7 @@ export async function runInteractiveOnboarding(
|
||||
const tokenInput = guardCancel(
|
||||
await text({
|
||||
message: "Gateway token (blank to generate)",
|
||||
placeholder: "Needed for multi-machine or non-loopback access",
|
||||
initialValue: randomToken(),
|
||||
}),
|
||||
runtime,
|
||||
@@ -375,7 +384,11 @@ export async function runInteractiveOnboarding(
|
||||
...nextConfig,
|
||||
gateway: {
|
||||
...nextConfig.gateway,
|
||||
auth: { ...nextConfig.gateway?.auth, mode: "token" },
|
||||
auth: {
|
||||
...nextConfig.gateway?.auth,
|
||||
mode: "token",
|
||||
token: gatewayToken,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -351,6 +351,8 @@ export type GatewayAuthMode = "token" | "password";
|
||||
export type GatewayAuthConfig = {
|
||||
/** Authentication mode for Gateway connections. Defaults to token when set. */
|
||||
mode?: GatewayAuthMode;
|
||||
/** Shared token for token mode (stored locally for CLI auth). */
|
||||
token?: string;
|
||||
/** Shared password for password mode (consider env instead). */
|
||||
password?: string;
|
||||
/** Allow Tailscale identity headers when serve mode is enabled. */
|
||||
@@ -1097,6 +1099,7 @@ const ClawdisSchema = z.object({
|
||||
auth: z
|
||||
.object({
|
||||
mode: z.union([z.literal("token"), z.literal("password")]).optional(),
|
||||
token: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
allowTailscale: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
|
||||
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",
|
||||
"gateway auth mode is token, but no token was configured (set gateway.auth.token or CLAWDIS_GATEWAY_TOKEN)",
|
||||
);
|
||||
}
|
||||
if (auth.mode === "password" && !auth.password) {
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function callGateway<T = unknown>(
|
||||
): Promise<T> {
|
||||
const timeoutMs = opts.timeoutMs ?? 10_000;
|
||||
const config = loadConfig();
|
||||
const remote =
|
||||
config.gateway?.mode === "remote" ? config.gateway.remote : undefined;
|
||||
const isRemoteMode = config.gateway?.mode === "remote";
|
||||
const remote = isRemoteMode ? config.gateway.remote : undefined;
|
||||
const url =
|
||||
(typeof opts.url === "string" && opts.url.trim().length > 0
|
||||
? opts.url.trim()
|
||||
@@ -39,9 +39,15 @@ export async function callGateway<T = unknown>(
|
||||
(typeof opts.token === "string" && opts.token.trim().length > 0
|
||||
? opts.token.trim()
|
||||
: undefined) ||
|
||||
(typeof remote?.token === "string" && remote.token.trim().length > 0
|
||||
? remote.token.trim()
|
||||
: undefined);
|
||||
(isRemoteMode
|
||||
? typeof remote?.token === "string" && remote.token.trim().length > 0
|
||||
? remote.token.trim()
|
||||
: undefined
|
||||
: process.env.CLAWDIS_GATEWAY_TOKEN?.trim() ||
|
||||
(typeof config.gateway?.auth?.token === "string" &&
|
||||
config.gateway.auth.token.trim().length > 0
|
||||
? config.gateway.auth.token.trim()
|
||||
: undefined));
|
||||
const password =
|
||||
(typeof opts.password === "string" && opts.password.trim().length > 0
|
||||
? opts.password.trim()
|
||||
|
||||
@@ -660,7 +660,6 @@ type DedupeEntry = {
|
||||
error?: ErrorShape;
|
||||
};
|
||||
|
||||
const getGatewayToken = () => process.env.CLAWDIS_GATEWAY_TOKEN;
|
||||
|
||||
function formatForLog(value: unknown): string {
|
||||
try {
|
||||
@@ -1371,7 +1370,8 @@ export async function startGatewayServer(
|
||||
...tailscaleOverrides,
|
||||
};
|
||||
const tailscaleMode = tailscaleConfig.mode ?? "off";
|
||||
const token = getGatewayToken();
|
||||
const token =
|
||||
authConfig.token ?? process.env.CLAWDIS_GATEWAY_TOKEN ?? undefined;
|
||||
const password =
|
||||
authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined;
|
||||
const authMode: ResolvedGatewayAuth["mode"] =
|
||||
|
||||
Reference in New Issue
Block a user