fix: update gateway auth docs and clients

This commit is contained in:
Peter Steinberger
2026-01-11 01:51:07 +01:00
parent d33285a9cd
commit b0b4b33b6b
28 changed files with 283 additions and 67 deletions

View File

@@ -92,7 +92,11 @@ import {
type VerboseLevel,
} from "./thinking.js";
import { SILENT_REPLY_TOKEN } from "./tokens.js";
import { isAudio, transcribeInboundAudio } from "./transcription.js";
import {
hasAudioTranscriptionConfig,
isAudio,
transcribeInboundAudio,
} from "./transcription.js";
import type { GetReplyOptions, ReplyPayload } from "./types.js";
export {
@@ -367,7 +371,7 @@ export async function getReplyFromConfig(
opts?.onTypingController?.(typing);
let transcribedText: string | undefined;
if (cfg.audio?.transcription && isAudio(ctx.MediaType)) {
if (hasAudioTranscriptionConfig(cfg) && isAudio(ctx.MediaType)) {
const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
if (transcribed?.text) {
transcribedText = transcribed.text;

View File

@@ -37,10 +37,12 @@ describe("transcribeInboundAudio", () => {
vi.stubGlobal("fetch", fetchMock);
const cfg = {
audio: {
transcription: {
command: ["echo", "{{MediaPath}}"],
timeoutSeconds: 5,
tools: {
audio: {
transcription: {
args: ["echo", "{{MediaPath}}"],
timeoutSeconds: 5,
},
},
},
};

View File

@@ -438,6 +438,11 @@ export function buildProgram() {
"Run without prompts (safe migrations only)",
false,
)
.option(
"--generate-gateway-token",
"Generate and configure a gateway token",
false,
)
.option("--deep", "Scan system services for extra gateway installs", false)
.action(async (opts) => {
try {
@@ -447,6 +452,7 @@ export function buildProgram() {
repair: Boolean(opts.repair),
force: Boolean(opts.force),
nonInteractive: Boolean(opts.nonInteractive),
generateGatewayToken: Boolean(opts.generateGatewayToken),
deep: Boolean(opts.deep),
});
} catch (err) {

View File

@@ -159,10 +159,15 @@ async function promptGatewayConfig(
await select({
message: "Gateway auth",
options: [
{ value: "off", label: "Off (loopback only)" },
{ value: "token", label: "Token" },
{
value: "off",
label: "Off (loopback only)",
hint: "Not recommended unless you fully trust local processes",
},
{ value: "token", label: "Token", hint: "Recommended default" },
{ value: "password", label: "Password" },
],
initialValue: "token",
}),
runtime,
) as "off" | "token" | "password";

View File

@@ -14,6 +14,7 @@ export type DoctorOptions = {
deep?: boolean;
repair?: boolean;
force?: boolean;
generateGatewayToken?: boolean;
};
export type DoctorPrompter = {

View File

@@ -384,7 +384,7 @@ export async function runNonInteractiveOnboarding(
? (opts.gatewayPort as number)
: resolveGatewayPort(baseConfig);
let bind = opts.gatewayBind ?? "loopback";
let authMode = opts.gatewayAuth ?? "off";
let authMode = opts.gatewayAuth ?? "token";
const tailscaleMode = opts.tailscale ?? "off";
const tailscaleResetOnExit = Boolean(opts.tailscaleResetOnExit);

View File

@@ -46,6 +46,33 @@ const mergeMissing = (
}
};
const AUDIO_TRANSCRIPTION_CLI_ALLOWLIST = new Set(["whisper"]);
const mapLegacyAudioTranscription = (
value: unknown,
): Record<string, unknown> | null => {
const transcriber = getRecord(value);
const command = Array.isArray(transcriber?.command)
? transcriber?.command
: null;
if (!command || command.length === 0) return null;
const rawExecutable = String(command[0] ?? "").trim();
if (!rawExecutable) return null;
const executableName = rawExecutable.split(/[\\/]/).pop() ?? rawExecutable;
if (!AUDIO_TRANSCRIPTION_CLI_ALLOWLIST.has(executableName)) return null;
const args = command.slice(1).map((part) => String(part));
const timeoutSeconds =
typeof transcriber?.timeoutSeconds === "number"
? transcriber?.timeoutSeconds
: undefined;
const result: Record<string, unknown> = {};
if (args.length > 0) result.args = args;
if (timeoutSeconds !== undefined) result.timeoutSeconds = timeoutSeconds;
return result;
};
const getAgentsList = (agents: Record<string, unknown> | null) => {
const list = agents?.list;
return Array.isArray(list) ? list : [];
@@ -137,7 +164,7 @@ const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
{
path: ["routing", "transcribeAudio"],
message:
"routing.transcribeAudio was moved; use audio.transcription instead (run `clawdbot doctor` to migrate).",
"routing.transcribeAudio was moved; use tools.audio.transcription instead (run `clawdbot doctor` to migrate).",
},
{
path: ["telegram", "requireMention"],
@@ -701,18 +728,57 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
}
if (routing.transcribeAudio !== undefined) {
const audio = ensureRecord(raw, "audio");
if (audio.transcription === undefined) {
audio.transcription = routing.transcribeAudio;
changes.push("Moved routing.transcribeAudio → audio.transcription.");
const mapped = mapLegacyAudioTranscription(routing.transcribeAudio);
if (mapped) {
const tools = ensureRecord(raw, "tools");
const toolsAudio = ensureRecord(tools, "audio");
if (toolsAudio.transcription === undefined) {
toolsAudio.transcription = mapped;
changes.push(
"Moved routing.transcribeAudio → tools.audio.transcription.",
);
} else {
changes.push(
"Removed routing.transcribeAudio (tools.audio.transcription already set).",
);
}
} else {
changes.push(
"Removed routing.transcribeAudio (audio.transcription already set).",
"Removed routing.transcribeAudio (unsupported transcription CLI).",
);
}
delete routing.transcribeAudio;
}
const audio = getRecord(raw.audio);
if (audio?.transcription !== undefined) {
const mapped = mapLegacyAudioTranscription(audio.transcription);
if (mapped) {
const tools = ensureRecord(raw, "tools");
const toolsAudio = ensureRecord(tools, "audio");
if (toolsAudio.transcription === undefined) {
toolsAudio.transcription = mapped;
changes.push(
"Moved audio.transcription → tools.audio.transcription.",
);
} else {
changes.push(
"Removed audio.transcription (tools.audio.transcription already set).",
);
}
delete audio.transcription;
if (Object.keys(audio).length === 0) delete raw.audio;
else raw.audio = audio;
} else {
delete audio.transcription;
changes.push(
"Removed audio.transcription (unsupported transcription CLI).",
);
if (Object.keys(audio).length === 0) delete raw.audio;
else raw.audio = audio;
}
}
if (Object.keys(routing).length === 0) {
delete raw.routing;
}

View File

@@ -915,6 +915,13 @@ export type AgentToolsConfig = {
export type ToolsConfig = {
allow?: string[];
deny?: string[];
audio?: {
transcription?: {
/** CLI args (template-enabled). */
args?: string[];
timeoutSeconds?: number;
};
};
agentToAgent?: {
/** Enable agent-to-agent messaging tools. Default: false. */
enabled?: boolean;
@@ -1023,6 +1030,7 @@ export type BroadcastConfig = {
};
export type AudioConfig = {
/** @deprecated Use tools.audio.transcription instead. */
transcription?: {
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
command: string[];

View File

@@ -1,6 +1,7 @@
import { z } from "zod";
import { parseDurationMs } from "../cli/parse-duration.js";
import { isSafeExecutableValue } from "../infra/exec-safety.js";
const ModelApiSchema = z.union([
z.literal("openai-completions"),
@@ -179,7 +180,16 @@ const QueueSchema = z
const TranscribeAudioSchema = z
.object({
command: z.array(z.string()),
command: z.array(z.string()).superRefine((value, ctx) => {
const executable = value[0];
if (!isSafeExecutableValue(executable)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [0],
message: "expected safe executable name or path",
});
}
}),
timeoutSeconds: z.number().int().positive().optional(),
})
.optional();
@@ -188,6 +198,17 @@ const HexColorSchema = z
.string()
.regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
const ExecutableTokenSchema = z
.string()
.refine(isSafeExecutableValue, "expected safe executable name or path");
const ToolsAudioTranscriptionSchema = z
.object({
args: z.array(z.string()).optional(),
timeoutSeconds: z.number().int().positive().optional(),
})
.optional();
const TelegramTopicSchema = z.object({
requireMention: z.boolean().optional(),
skills: z.array(z.string()).optional(),
@@ -422,7 +443,7 @@ const SignalAccountSchemaBase = z.object({
httpUrl: z.string().optional(),
httpHost: z.string().optional(),
httpPort: z.number().int().positive().optional(),
cliPath: z.string().optional(),
cliPath: ExecutableTokenSchema.optional(),
autoStart: z.boolean().optional(),
receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(),
ignoreAttachments: z.boolean().optional(),
@@ -470,7 +491,7 @@ const IMessageAccountSchemaBase = z.object({
name: z.string().optional(),
capabilities: z.array(z.string()).optional(),
enabled: z.boolean().optional(),
cliPath: z.string().optional(),
cliPath: ExecutableTokenSchema.optional(),
dbPath: z.string().optional(),
service: z
.union([z.literal("imessage"), z.literal("sms"), z.literal("auto")])
@@ -819,6 +840,11 @@ const ToolsSchema = z
.object({
allow: z.array(z.string()).optional(),
deny: z.array(z.string()).optional(),
audio: z
.object({
transcription: ToolsAudioTranscriptionSchema,
})
.optional(),
agentToAgent: z
.object({
enabled: z.boolean().optional(),