fix: persist gateway token for local CLI auth
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
- 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).
|
- 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).
|
- 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.
|
- WebChat: stream live updates for sessions even when runs start outside the chat UI.
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ Defaults:
|
|||||||
mode: "local", // or "remote"
|
mode: "local", // or "remote"
|
||||||
bind: "loopback",
|
bind: "loopback",
|
||||||
// controlUi: { enabled: true }
|
// 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" }
|
// tailscale: { mode: "off" | "serve" | "funnel" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,6 +566,7 @@ Notes:
|
|||||||
|
|
||||||
Auth and Tailscale:
|
Auth and Tailscale:
|
||||||
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
- `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).
|
- 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.password` can be set here, or via `CLAWDIS_GATEWAY_PASSWORD` (recommended).
|
||||||
- `gateway.auth.allowTailscale` controls whether Tailscale identity headers can satisfy auth.
|
- `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.
|
- **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**.
|
- **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).
|
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)
|
## 2) Local-only: Connect Claude (Anthropic OAuth)
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ It does **not** install or change anything on the remote host.
|
|||||||
|
|
||||||
4) **Gateway**
|
4) **Gateway**
|
||||||
- Port, bind, auth mode, tailscale exposure.
|
- 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.
|
- Non‑loopback binds require auth.
|
||||||
|
|
||||||
5) **Providers**
|
5) **Providers**
|
||||||
|
|||||||
@@ -280,8 +280,16 @@ export async function runInteractiveOnboarding(
|
|||||||
await select({
|
await select({
|
||||||
message: "Gateway auth",
|
message: "Gateway auth",
|
||||||
options: [
|
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" },
|
{ value: "password", label: "Password" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -344,6 +352,7 @@ export async function runInteractiveOnboarding(
|
|||||||
const tokenInput = guardCancel(
|
const tokenInput = guardCancel(
|
||||||
await text({
|
await text({
|
||||||
message: "Gateway token (blank to generate)",
|
message: "Gateway token (blank to generate)",
|
||||||
|
placeholder: "Needed for multi-machine or non-loopback access",
|
||||||
initialValue: randomToken(),
|
initialValue: randomToken(),
|
||||||
}),
|
}),
|
||||||
runtime,
|
runtime,
|
||||||
@@ -375,7 +384,11 @@ export async function runInteractiveOnboarding(
|
|||||||
...nextConfig,
|
...nextConfig,
|
||||||
gateway: {
|
gateway: {
|
||||||
...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 = {
|
export type GatewayAuthConfig = {
|
||||||
/** Authentication mode for Gateway connections. Defaults to token when set. */
|
/** Authentication mode for Gateway connections. Defaults to token when set. */
|
||||||
mode?: GatewayAuthMode;
|
mode?: GatewayAuthMode;
|
||||||
|
/** Shared token for token mode (stored locally for CLI auth). */
|
||||||
|
token?: string;
|
||||||
/** Shared password for password mode (consider env instead). */
|
/** Shared password for password mode (consider env instead). */
|
||||||
password?: string;
|
password?: string;
|
||||||
/** Allow Tailscale identity headers when serve mode is enabled. */
|
/** Allow Tailscale identity headers when serve mode is enabled. */
|
||||||
@@ -1097,6 +1099,7 @@ const ClawdisSchema = z.object({
|
|||||||
auth: z
|
auth: z
|
||||||
.object({
|
.object({
|
||||||
mode: z.union([z.literal("token"), z.literal("password")]).optional(),
|
mode: z.union([z.literal("token"), z.literal("password")]).optional(),
|
||||||
|
token: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
allowTailscale: z.boolean().optional(),
|
allowTailscale: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
|
|||||||
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
|
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
|
||||||
if (auth.mode === "token" && !auth.token) {
|
if (auth.mode === "token" && !auth.token) {
|
||||||
throw new Error(
|
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) {
|
if (auth.mode === "password" && !auth.password) {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export async function callGateway<T = unknown>(
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const timeoutMs = opts.timeoutMs ?? 10_000;
|
const timeoutMs = opts.timeoutMs ?? 10_000;
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const remote =
|
const isRemoteMode = config.gateway?.mode === "remote";
|
||||||
config.gateway?.mode === "remote" ? config.gateway.remote : undefined;
|
const remote = isRemoteMode ? config.gateway.remote : undefined;
|
||||||
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()
|
||||||
@@ -39,9 +39,15 @@ export async function callGateway<T = unknown>(
|
|||||||
(typeof opts.token === "string" && opts.token.trim().length > 0
|
(typeof opts.token === "string" && opts.token.trim().length > 0
|
||||||
? opts.token.trim()
|
? opts.token.trim()
|
||||||
: undefined) ||
|
: undefined) ||
|
||||||
(typeof remote?.token === "string" && remote.token.trim().length > 0
|
(isRemoteMode
|
||||||
? remote.token.trim()
|
? typeof remote?.token === "string" && remote.token.trim().length > 0
|
||||||
: undefined);
|
? 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 =
|
const password =
|
||||||
(typeof opts.password === "string" && opts.password.trim().length > 0
|
(typeof opts.password === "string" && opts.password.trim().length > 0
|
||||||
? opts.password.trim()
|
? opts.password.trim()
|
||||||
|
|||||||
@@ -660,7 +660,6 @@ type DedupeEntry = {
|
|||||||
error?: ErrorShape;
|
error?: ErrorShape;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGatewayToken = () => process.env.CLAWDIS_GATEWAY_TOKEN;
|
|
||||||
|
|
||||||
function formatForLog(value: unknown): string {
|
function formatForLog(value: unknown): string {
|
||||||
try {
|
try {
|
||||||
@@ -1371,7 +1370,8 @@ export async function startGatewayServer(
|
|||||||
...tailscaleOverrides,
|
...tailscaleOverrides,
|
||||||
};
|
};
|
||||||
const tailscaleMode = tailscaleConfig.mode ?? "off";
|
const tailscaleMode = tailscaleConfig.mode ?? "off";
|
||||||
const token = getGatewayToken();
|
const token =
|
||||||
|
authConfig.token ?? process.env.CLAWDIS_GATEWAY_TOKEN ?? undefined;
|
||||||
const password =
|
const password =
|
||||||
authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined;
|
authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined;
|
||||||
const authMode: ResolvedGatewayAuth["mode"] =
|
const authMode: ResolvedGatewayAuth["mode"] =
|
||||||
|
|||||||
Reference in New Issue
Block a user