Compare commits

..
11 changed files with 12 additions and 146 deletions

No files matched your search

-1
View File
@@ -11,7 +11,6 @@ Docs: https://docs.clawd.bot
### Fixes ### Fixes
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean. - Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
- UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283) — thanks @bradleypriest. - UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283) — thanks @bradleypriest.
- Models: allow Bedrock custom providers without API keys and preserve inline provider IDs. (#1286) — thanks @alauppe.
## 2026.1.19-2 ## 2026.1.19-2
-90
View File
@@ -280,94 +280,4 @@ describe("getApiKeyForModel", () => {
} }
} }
}); });
it("accepts AWS profile auth for amazon-bedrock", async () => {
const previousProfile = process.env.AWS_PROFILE;
const previousAccess = process.env.AWS_ACCESS_KEY_ID;
const previousSecret = process.env.AWS_SECRET_ACCESS_KEY;
const previousBearer = process.env.AWS_BEARER_TOKEN_BEDROCK;
try {
process.env.AWS_PROFILE = "bedrock-test";
delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY;
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
});
expect(resolved.apiKey).toBe("<authenticated>");
expect(resolved.source).toContain("AWS_PROFILE");
} finally {
if (previousProfile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previousProfile;
}
if (previousAccess === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previousAccess;
}
if (previousSecret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previousSecret;
}
if (previousBearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previousBearer;
}
}
});
it("allows amazon-bedrock without an API key", async () => {
const previousProfile = process.env.AWS_PROFILE;
const previousAccess = process.env.AWS_ACCESS_KEY_ID;
const previousSecret = process.env.AWS_SECRET_ACCESS_KEY;
const previousBearer = process.env.AWS_BEARER_TOKEN_BEDROCK;
try {
delete process.env.AWS_PROFILE;
delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY;
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
});
expect(resolved.apiKey).toBe("<authenticated>");
expect(resolved.source).toBe("aws-sdk");
} finally {
if (previousProfile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previousProfile;
}
if (previousAccess === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previousAccess;
}
if (previousSecret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previousSecret;
}
if (previousBearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previousBearer;
}
}
});
}); });
-26
View File
@@ -36,7 +36,6 @@ export async function resolveApiKeyForProvider(params: {
}): Promise<{ apiKey: string; profileId?: string; source: string }> { }): Promise<{ apiKey: string; profileId?: string; source: string }> {
const { provider, cfg, profileId, preferredProfile } = params; const { provider, cfg, profileId, preferredProfile } = params;
const store = params.store ?? ensureAuthProfileStore(params.agentDir); const store = params.store ?? ensureAuthProfileStore(params.agentDir);
const normalized = normalizeProviderId(provider);
if (profileId) { if (profileId) {
const resolved = await resolveApiKeyForProfile({ const resolved = await resolveApiKeyForProfile({
@@ -89,10 +88,6 @@ export async function resolveApiKeyForProvider(params: {
return { apiKey: customKey, source: "models.json" }; return { apiKey: customKey, source: "models.json" };
} }
if (normalized === "amazon-bedrock") {
return { apiKey: "<authenticated>", source: "aws-sdk" };
}
if (provider === "openai") { if (provider === "openai") {
const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0; const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0;
if (hasCodex) { if (hasCodex) {
@@ -125,12 +120,6 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`; const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
return { apiKey: value, source }; return { apiKey: value, source };
}; };
const pickPresent = (envVar: string): EnvApiKeyResult | null => {
const value = process.env[envVar]?.trim();
if (!value) return null;
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
return { apiKey: "<authenticated>", source };
};
if (normalized === "github-copilot") { if (normalized === "github-copilot") {
return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN"); return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN");
@@ -154,21 +143,6 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
return { apiKey: envKey, source: "gcloud adc" }; return { apiKey: envKey, source: "gcloud adc" };
} }
if (normalized === "amazon-bedrock") {
return (
pickPresent("AWS_BEARER_TOKEN_BEDROCK") ??
pickPresent("AWS_PROFILE") ??
(process.env.AWS_ACCESS_KEY_ID?.trim() && process.env.AWS_SECRET_ACCESS_KEY?.trim()
? {
apiKey: "<authenticated>",
source: applied.has("AWS_ACCESS_KEY_ID")
? "shell env: AWS_ACCESS_KEY_ID"
: "env: AWS_ACCESS_KEY_ID",
}
: null)
);
}
if (normalized === "opencode") { if (normalized === "opencode") {
return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY"); return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY");
} }
+1 -4
View File
@@ -143,10 +143,7 @@ export function normalizeProviders(params: {
provider: normalizedKey, provider: normalizedKey,
store: authStore, store: authStore,
}); });
const apiKey = const apiKey = fromEnv ?? fromProfiles;
fromEnv ??
fromProfiles ??
(normalizedKey === "amazon-bedrock" ? "AWS_PROFILE" : undefined);
if (apiKey?.trim()) { if (apiKey?.trim()) {
mutated = true; mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey }; normalizedProvider = { ...normalizedProvider, apiKey };
+4 -4
View File
@@ -39,10 +39,10 @@ export function resolveModel(
if (!model) { if (!model) {
const providers = cfg?.models?.providers ?? {}; const providers = cfg?.models?.providers ?? {};
const inlineModels = const inlineModels =
providers[provider]?.models?.map((entry) => ({ ...entry, provider })) ?? providers[provider]?.models ??
Object.entries(providers).flatMap(([providerId, entry]) => Object.values(providers)
(entry?.models ?? []).map((modelEntry) => ({ ...modelEntry, provider: providerId })), .flatMap((entry) => entry?.models ?? [])
); .map((entry) => ({ ...entry, provider }));
const inlineMatch = inlineModels.find((entry) => entry.id === modelId); const inlineMatch = inlineModels.find((entry) => entry.id === modelId);
if (inlineMatch) { if (inlineMatch) {
const normalized = normalizeModelCompat(inlineMatch as Model<Api>); const normalized = normalizeModelCompat(inlineMatch as Model<Api>);
+1 -1
View File
@@ -16,7 +16,7 @@ export async function checkGatewayHealth(params: {
typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 10_000; typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 10_000;
let healthOk = false; let healthOk = false;
try { try {
await healthCommand({ json: false, timeoutMs, config: params.cfg }, params.runtime); await healthCommand({ json: false, timeoutMs }, params.runtime);
healthOk = true; healthOk = true;
} catch (err) { } catch (err) {
const message = String(err); const message = String(err);
+1 -10
View File
@@ -10,7 +10,7 @@ import {
resolveHooksGmailModel, resolveHooksGmailModel,
} from "../agents/model-selection.js"; } from "../agents/model-selection.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, writeConfigFile } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js";
import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayService } from "../daemon/service.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js"; import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
@@ -269,14 +269,5 @@ export async function doctorCommand(
} }
} }
const finalSnapshot = await readConfigFileSnapshot();
if (finalSnapshot.exists && !finalSnapshot.valid) {
runtime.error("Invalid config:");
for (const issue of finalSnapshot.issues) {
const path = issue.path || "<root>";
runtime.error(`- ${path}: ${issue.message}`);
}
}
outro("Doctor complete."); outro("Doctor complete.");
} }
+3 -5
View File
@@ -3,7 +3,6 @@ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js"; import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js"; import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
import { withProgress } from "../cli/progress.js"; import { withProgress } from "../cli/progress.js";
import type { ClawdbotConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js"; import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
@@ -502,10 +501,9 @@ export async function getHealthSnapshot(params?: {
} }
export async function healthCommand( export async function healthCommand(
opts: { json?: boolean; timeoutMs?: number; verbose?: boolean; config?: ClawdbotConfig }, opts: { json?: boolean; timeoutMs?: number; verbose?: boolean },
runtime: RuntimeEnv, runtime: RuntimeEnv,
) { ) {
const cfg = opts.config ?? loadConfig();
// Always query the running gateway; do not open a direct Baileys socket here. // Always query the running gateway; do not open a direct Baileys socket here.
const summary = await withProgress( const summary = await withProgress(
{ {
@@ -518,7 +516,6 @@ export async function healthCommand(
method: "health", method: "health",
params: opts.verbose ? { probe: true } : undefined, params: opts.verbose ? { probe: true } : undefined,
timeoutMs: opts.timeoutMs, timeoutMs: opts.timeoutMs,
config: cfg,
}), }),
); );
// Gateway reachability defines success; channel issues are reported but not fatal here. // Gateway reachability defines success; channel issues are reported but not fatal here.
@@ -529,12 +526,13 @@ export async function healthCommand(
} else { } else {
const debugEnabled = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_HEALTH); const debugEnabled = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_HEALTH);
if (opts.verbose) { if (opts.verbose) {
const details = buildGatewayConnectionDetails({ config: cfg }); const details = buildGatewayConnectionDetails();
runtime.log(info("Gateway connection:")); runtime.log(info("Gateway connection:"));
for (const line of details.message.split("\n")) { for (const line of details.message.split("\n")) {
runtime.log(` ${line}`); runtime.log(` ${line}`);
} }
} }
const cfg = loadConfig();
const localAgents = resolveAgentOrder(cfg); const localAgents = resolveAgentOrder(cfg);
const defaultAgentId = summary.defaultAgentId ?? localAgents.defaultAgentId; const defaultAgentId = summary.defaultAgentId ?? localAgents.defaultAgentId;
const agents = Array.isArray(summary.agents) ? summary.agents : []; const agents = Array.isArray(summary.agents) ? summary.agents : [];
+1 -2
View File
@@ -3,8 +3,7 @@ export type ModelApi =
| "openai-responses" | "openai-responses"
| "anthropic-messages" | "anthropic-messages"
| "google-generative-ai" | "google-generative-ai"
| "github-copilot" | "github-copilot";
| "bedrock-converse-stream";
export type ModelCompatConfig = { export type ModelCompatConfig = {
supportsStore?: boolean; supportsStore?: boolean;
-1
View File
@@ -8,7 +8,6 @@ export const ModelApiSchema = z.union([
z.literal("anthropic-messages"), z.literal("anthropic-messages"),
z.literal("google-generative-ai"), z.literal("google-generative-ai"),
z.literal("github-copilot"), z.literal("github-copilot"),
z.literal("bedrock-converse-stream"),
]); ]);
export const ModelCompatSchema = z export const ModelCompatSchema = z
+1 -2
View File
@@ -22,7 +22,6 @@ export type CallGatewayOptions = {
url?: string; url?: string;
token?: string; token?: string;
password?: string; password?: string;
config?: ClawdbotConfig;
method: string; method: string;
params?: unknown; params?: unknown;
expectFinal?: boolean; expectFinal?: boolean;
@@ -110,7 +109,7 @@ export function buildGatewayConnectionDetails(
export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promise<T> { export async function callGateway<T = unknown>(opts: CallGatewayOptions): Promise<T> {
const timeoutMs = opts.timeoutMs ?? 10_000; const timeoutMs = opts.timeoutMs ?? 10_000;
const config = opts.config ?? loadConfig(); const config = loadConfig();
const isRemoteMode = config.gateway?.mode === "remote"; const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode ? config.gateway?.remote : undefined; const remote = isRemoteMode ? config.gateway?.remote : undefined;
const urlOverride = const urlOverride =