fix: update gateway auth docs and clients
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -14,6 +14,7 @@ export type DoctorOptions = {
|
||||
deep?: boolean;
|
||||
repair?: boolean;
|
||||
force?: boolean;
|
||||
generateGatewayToken?: boolean;
|
||||
};
|
||||
|
||||
export type DoctorPrompter = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user