Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5d32e09
security: harden secrets, auth, injection, crypto, and fail-closed gates
Mar 31, 2026
579ba28
security: phase 2 — crypto, policy, logging, and validation hardening
Mar 31, 2026
3a951d9
fix: sidecar admin token race — generate before sidecar starts, pass …
Mar 31, 2026
5b9a4ca
fix: spawn latency ~2min→~15s + registry path allowlist
Mar 31, 2026
e904d06
fix: router→sidecar auth for trust/audit endpoints
Mar 31, 2026
7ee0676
feat: image generation via standard OpenAI Images API
Mar 31, 2026
c13f302
Feat: unified /openai/v1/ proxy + Responses API + auto-fallback
Mar 31, 2026
7c7b14b
Fix: secure parent-mediated trust delegation for mesh siblings
Mar 31, 2026
fd3e065
Perf: cache Responses-only models to skip redundant chat/completions …
Mar 31, 2026
42f6223
Fix: complete chat↔Responses API format translation
Mar 31, 2026
e504597
Fix: don't force-kill running sub-agent containers on re-spawn
Mar 31, 2026
d0387ff
Fix: reuse existing sandbox on AKS instead of erroring on 409
Mar 31, 2026
0826e48
Perf: optimize mesh handshake latency (no crypto changes)
Mar 31, 2026
8dcaa39
Fix: responses_to_chat_body skipped conversion due to 'error: null'
Mar 31, 2026
bc4b6cc
Fix: SSE keepalive for Responses API to prevent client timeouts
Mar 31, 2026
e157a0f
Fix sub-agent task execution: bypass OpenClaw gateway, add mesh_send …
Mar 31, 2026
877388b
Fix: entrypoint crash when /sandbox/.bashrc missing on AKS
Apr 1, 2026
c14e618
Fix: enable azureclaw plugin in OpenClaw config entries
Apr 1, 2026
be80eef
Fix: Foundry project endpoint lost on cache reload
Apr 1, 2026
9207851
feat: AzureClaw identity via OpenClaw config + MEMORY.md
Apr 1, 2026
d4f0622
feat: unified secrets store for all credentials
Apr 1, 2026
1ce43e6
feat: azureclaw add resolves tokens from secrets.json
Apr 1, 2026
89d2c38
feat: guided credential wizard + multi-token + operator integration
Apr 1, 2026
23be185
feat: add discover + mesh_inbox to sub-agents, guided credentials wizard
Apr 1, 2026
301e980
fix: restore delegateToNativeAgent as primary sub-agent path
Apr 1, 2026
b9162e1
feat: AGT as sole policy authority for sub-agent tasks
Apr 1, 2026
47b5170
fix: use max_completion_tokens for AOAI compatibility
Apr 1, 2026
ad31dc5
fix: build errors in credentials wizard and exec_command handler
Apr 1, 2026
58e75f7
fix: resolveSecret falls back to dot-suffixed variants
Apr 1, 2026
92cef1c
feat: channel variant syntax for credential selection
Apr 1, 2026
aed4e1d
fix: image generation timeout + Go 1.25 + npm audit fix
Apr 1, 2026
bce1a5d
fix: disable OpenClaw exec approvals via config
Apr 1, 2026
ca169af
feat: register image generation provider for inline rendering
Apr 1, 2026
3f279a9
feat: inline image rendering via OpenClaw's native image_generate
Apr 1, 2026
ab1f215
style: cargo fmt
Apr 1, 2026
211fe88
fix: address code quality review comments
Apr 1, 2026
1f0b170
feat: operator spawn UX — show token variant names and allow-from sup…
Apr 1, 2026
e0aeff2
ci: use larger runner for container image scan
Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:

container-scan:
name: Container Image Scan
runs-on: ubuntu-latest
runs-on: ubuntu-latest-xl
permissions:
contents: read
security-events: write
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/policies/azureclaw-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ rules:
condition:
field: action.full
operator: matches
value: "\\bnmap\\b|\\bnc\\s+-[elp]|\\bnetcat\\b|\\bsocat\\b|\\bnsenter\\b|\\bunshare\\b|\\bchroot\\b"
value: "(?:^|[\\s;|&(/])nmap(?:\\s|$|;|\\||&)|(?:^|[\\s;|&(/])nc\\s+-[elp]|(?:^|[\\s;|&(/])netcat(?:\\s|$|;|\\||&)|(?:^|[\\s;|&(/])socat(?:\\s|$|;|\\||&)|(?:^|[\\s;|&(/])nsenter(?:\\s|$|;|\\||&)|(?:^|[\\s;|&(/])unshare(?:\\s|$|;|\\||&)|(?:^|[\\s;|&(/])chroot(?:\\s|$|;|\\||&)"
action: deny
priority: 5

Expand Down
52 changes: 36 additions & 16 deletions cli/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import { loadContext } from "../config.js";
import { loadContext, resolveSecret } from "../config.js";

export function addCommand(): Command {
const cmd = new Command("add");
Expand All @@ -22,6 +22,7 @@ export function addCommand(): Command {
.option("--policy-profile <profile>", "AGT policy profile name", "default")
.option("--channels <channels>", "Channels to enable: telegram,slack,discord,whatsapp (comma-separated)")
.option("--telegram-token <token>", "Telegram bot token (from BotFather)")
.option("--telegram-allow-from <ids>", "Telegram user IDs allowed to DM (comma-separated numeric IDs)")
.option("--slack-token <token>", "Slack bot OAuth token")
.option("--discord-token <token>", "Discord bot token")
.option("--skills <skills>", "Skills to activate: browser,github,summarize,weather (comma-separated)")
Expand Down Expand Up @@ -138,40 +139,59 @@ export function addCommand(): Command {
const channels = options.channels.split(",").map((c: string) => c.trim().toLowerCase());
const envSecrets: Record<string, string> = {};

// Map channel names to secret keys for resolveSecret lookup
const channelSecretKeys: Record<string, string> = {
telegram: "telegram-token",
slack: "slack-token",
discord: "discord-token",
};

for (const channel of channels) {
if (!knownChannels.has(channel)) {
console.error(chalk.yellow(` ⚠ Unknown channel '${channel}' — skipping`));
continue;
}
// Map channel token flags to env var names
const tokenFlag = channelTokenFlags[channel];
if (tokenFlag && options[tokenFlag]) {
envSecrets[channelEnvVars[channel]] = options[tokenFlag];
const secretKey = channelSecretKeys[channel];
// Resolve: CLI flag > secrets.json > env var
const resolved = secretKey
? resolveSecret(tokenFlag ? options[tokenFlag] : undefined, secretKey)
: undefined;
if (resolved) {
envSecrets[channelEnvVars[channel]] = resolved;
} else if (channel === "whatsapp") {
envSecrets[channelEnvVars[channel]] = "true";
} else if (tokenFlag && !options[tokenFlag]) {
console.error(chalk.yellow(` ⚠ Channel '${channel}' enabled but no --${channel}-token provided`));
} else if (tokenFlag && !resolved) {
console.error(chalk.yellow(` ⚠ Channel '${channel}' enabled but no token found (use --${channel}-token or 'azureclaw credentials set ${channel}-token <token>')`));
}
}

// Store for secret creation (NOT in CRD spec — entrypoint reads env vars)
channelEnvSecrets = envSecrets;

// Telegram allow-from (which user IDs can DM the bot)
if (channels.includes("telegram")) {
const allowFrom = resolveSecret(options.telegramAllowFrom, "telegram-allow-from");
if (allowFrom) channelEnvSecrets["TELEGRAM_ALLOW_FROM"] = allowFrom;
}
}

// Third-party plugin API keys — stored in the same K8s secret as channel tokens.
// The entrypoint auto-enables plugins when their env var is present.
const pluginKeyFlags: Record<string, { flag: string; env: string }> = {
brave: { flag: "braveApiKey", env: "BRAVE_API_KEY" },
tavily: { flag: "tavilyApiKey", env: "TAVILY_API_KEY" },
exa: { flag: "exaApiKey", env: "EXA_API_KEY" },
firecrawl: { flag: "firecrawlApiKey", env: "FIRECRAWL_API_KEY" },
perplexity: { flag: "perplexityApiKey", env: "PERPLEXITY_API_KEY" },
openai: { flag: "openaiApiKey", env: "OPENAI_API_KEY" },
// Resolve: CLI flag > secrets.json > env var
const pluginKeyFlags: Record<string, { flag: string; env: string; secretKey: string }> = {
brave: { flag: "braveApiKey", env: "BRAVE_API_KEY", secretKey: "brave-api-key" },
tavily: { flag: "tavilyApiKey", env: "TAVILY_API_KEY", secretKey: "tavily-api-key" },
exa: { flag: "exaApiKey", env: "EXA_API_KEY", secretKey: "exa-api-key" },
firecrawl: { flag: "firecrawlApiKey", env: "FIRECRAWL_API_KEY", secretKey: "firecrawl-api-key" },
perplexity: { flag: "perplexityApiKey", env: "PERPLEXITY_API_KEY", secretKey: "perplexity-api-key" },
openai: { flag: "openaiApiKey", env: "OPENAI_API_KEY", secretKey: "openai-api-key" },
};
const pluginSecrets: Record<string, string> = {};
for (const [, { flag, env }] of Object.entries(pluginKeyFlags)) {
if (options[flag]) {
pluginSecrets[env] = options[flag];
for (const [, { flag, env, secretKey }] of Object.entries(pluginKeyFlags)) {
const resolved = resolveSecret(options[flag], secretKey);
if (resolved) {
pluginSecrets[env] = resolved;
}
}

Expand Down
219 changes: 207 additions & 12 deletions cli/src/commands/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,229 @@
import { Command } from "commander";
import chalk from "chalk";
import { banner, section } from "../stepper.js";
import { promptAndSaveCredentials, CONFIG_FILE, CREDENTIALS_FILE } from "../config.js";
import {
promptAndSaveCredentials, SECRETS_FILE,
KNOWN_SECRETS, loadSecrets, setSecret, getSecret, deleteSecret, listSecretVariants,
} from "../config.js";
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

export function credentialsCommand(): Command {
const cmd = new Command("credentials");

cmd
.description(
"Set or update Azure OpenAI credentials (endpoint, API key, model)"
"Manage AzureClaw credentials (Azure OpenAI, channel tokens, API keys)"
)
.action(async () => {
const { default: inquirer } = await import("inquirer");

banner("AzureClaw · Credentials", "Secure AI Agent Runtime on Azure");

const creds = await promptAndSaveCredentials();
// Show current state
const secrets = loadSecrets();
if (Object.keys(secrets).length > 0) {
console.log(chalk.dim(" Currently stored:"));
for (const key of Object.keys(secrets).sort()) {
const val = secrets[key];
const masked = val.length > 8 ? "••••" + val.slice(-4) : "••••";
const info = KNOWN_SECRETS[key.includes(".") ? key.slice(0, key.indexOf(".")) : key];
const label = info ? chalk.dim(` (${info.label})`) : "";
console.log(` ${chalk.cyan(key)} = ${masked}${label}`);
}
console.log();
}

// Main menu
const categories = [
{ name: "Azure OpenAI — endpoint, model, API key", value: "aoai" },
{ name: "Telegram — bot token, allowed users", value: "telegram" },
{ name: "Slack — bot OAuth token", value: "slack" },
{ name: "Discord — bot token", value: "discord" },
{ name: "Search APIs — Brave, Tavily, Exa, Perplexity", value: "search" },
{ name: "Other APIs — Firecrawl, OpenAI", value: "other" },
new inquirer.Separator(),
{ name: "Done", value: "done" },
];

while (true) {
const { category } = await inquirer.prompt([{
type: "list",
name: "category",
message: "What would you like to configure?",
choices: categories,
}]);

if (category === "done") break;

if (category === "aoai") {
await promptAndSaveCredentials({ heading: "Azure OpenAI" });
} else {
const promptMap: Record<string, Array<{ key: string; label: string; allowSuffix?: boolean }>> = {
telegram: [
{ key: "telegram-token", label: "Telegram bot token", allowSuffix: true },
{ key: "telegram-allow-from", label: "Telegram allowed user IDs (comma-separated)", allowSuffix: true },
],
slack: [
{ key: "slack-token", label: "Slack bot OAuth token", allowSuffix: true },
],
discord: [
{ key: "discord-token", label: "Discord bot token", allowSuffix: true },
],
search: [
{ key: "brave-api-key", label: "Brave Search API key" },
{ key: "tavily-api-key", label: "Tavily search API key" },
{ key: "exa-api-key", label: "Exa search API key" },
{ key: "perplexity-api-key", label: "Perplexity API key" },
],
other: [
{ key: "firecrawl-api-key", label: "Firecrawl API key" },
{ key: "openai-api-key", label: "OpenAI API key" },
],
};
const prompts = promptMap[category] || [];

for (const p of prompts) {
let finalKey = p.key;

section("Saved");
console.log(` Endpoint: ${chalk.bold(creds.endpoint)}`);
console.log(` Model: ${chalk.bold(creds.model)}`);
console.log(` Key: ${chalk.dim("••••" + creds.apiKey.slice(-4))}`);
console.log(` Config: ${chalk.dim(CONFIG_FILE)}`);
console.log(` Key file: ${chalk.dim(CREDENTIALS_FILE)} ${chalk.dim("(600)")}`);
// For tokens that support dot-suffix variants, ask for label
if (p.allowSuffix) {
const existing = listSecretVariants(p.key);
if (existing.length > 0) {
console.log(chalk.dim(` Existing: ${existing.map(v => v.key).join(", ")}`));
}
const { suffix } = await inquirer.prompt([{
type: "input",
name: "suffix",
message: `Label for this ${p.label} (e.g. "cloud", "dev", or blank for default):`,
filter: (v: string) => v.trim().toLowerCase().replace(/\s+/g, "-"),
}]);
if (suffix) finalKey = `${p.key}.${suffix}`;
}

const currentVal = getSecret(finalKey);
const currentHint = currentVal ? chalk.dim(` (current: ••••${currentVal.slice(-4)})`) : "";

const { value } = await inquirer.prompt([{
type: "password",
name: "value",
message: `${p.label}${currentHint}:`,
mask: "•",
}]);

if (value && value.trim()) {
setSecret(finalKey, value.trim());
const masked = value.length > 8 ? "••••" + value.slice(-4) : "••••";
console.log(chalk.green(` ✔ ${finalKey} = ${masked}`));
} else if (currentVal) {
console.log(chalk.dim(` Kept existing value for ${finalKey}`));
} else {
console.log(chalk.dim(` Skipped ${finalKey}`));
}
}
}
console.log();
}

section("Summary");
const final = loadSecrets();
const count = Object.keys(final).length;
console.log(` ${chalk.bold(String(count))} secret${count !== 1 ? "s" : ""} stored in ${chalk.dim(SECRETS_FILE)}`);
console.log();
section("Next Steps");
console.log(` Dev: ${chalk.cyan("azureclaw dev")}`);
console.log(` Prod: ${chalk.cyan("azureclaw up")}`);
console.log(` Re-run: ${chalk.cyan("azureclaw credentials")}`);
console.log(` Dev: ${chalk.cyan("azureclaw dev")} ${chalk.dim("— tokens auto-loaded")}`);
console.log(` Add: ${chalk.cyan("azureclaw add <name>")} ${chalk.dim("— tokens auto-loaded")}`);
console.log(` List: ${chalk.cyan("azureclaw credentials list")} ${chalk.dim("— show all (masked)")}`);
console.log();
});

// ─── set <key> [value] ───────────────────────────────────────────────────
const set = new Command("set");
set
.description("Store a secret locally (e.g. telegram-token, brave-api-key)")
.argument("<key>", `Secret key (${Object.keys(KNOWN_SECRETS).join(", ")})`)
.argument("[value]", "Secret value (omit for masked prompt)")
.action(async (key: string, value?: string) => {
const info = KNOWN_SECRETS[key];
// Strip dot-suffix for validation (telegram-token.cloud → telegram-token)
const baseKey = key.includes(".") ? key.slice(0, key.indexOf(".")) : key;
const baseInfo = KNOWN_SECRETS[baseKey];
if (!info && !baseInfo) {
console.log(chalk.yellow(` Warning: '${key}' is not a known secret key.`));
console.log(chalk.dim(` Known keys: ${Object.keys(KNOWN_SECRETS).join(", ")}`));
}

// If no value provided, prompt with masked input
if (!value) {
const label = (info || baseInfo)?.label || key;
const { default: inquirer } = await import("inquirer");
const answer = await inquirer.prompt([{
type: "password",
name: "secret",
message: `${label}:`,
mask: "•",
validate: (input: string) => input.length > 0 ? true : "Value cannot be empty",
}]);
value = answer.secret;
}

setSecret(key, value!);
const masked = value!.length > 8 ? "••••" + value!.slice(-4) : "••••";
console.log(chalk.green(` ✔ ${key} = ${masked}`));
console.log(chalk.dim(` Saved to ${SECRETS_FILE}`));
if (info || baseInfo) {
console.log(chalk.dim(` → env var: ${(info || baseInfo)!.env}`));
}
});
cmd.addCommand(set);

// ─── list ──────────────────────────────────────────────────────────────────
const list = new Command("list");
list
.description("List all stored secrets (values masked)")
.action(() => {
const secrets = loadSecrets();
const keys = Object.keys(secrets);
if (keys.length === 0) {
console.log(chalk.dim(" No secrets stored. Use: azureclaw credentials set <key> <value>"));
return;
}
console.log(chalk.bold("\n Stored secrets:\n"));
for (const key of keys.sort()) {
const val = secrets[key];
const masked = val.length > 8 ? "••••" + val.slice(-4) : "••••";
const info = KNOWN_SECRETS[key];
let label = "";
if (info) {
label = chalk.dim(` (${info.label})`);
} else {
// Check for dot-suffixed variant (e.g. telegram-token.cloud → "Telegram bot token")
const dotIdx = key.indexOf(".");
if (dotIdx > 0) {
const baseKey = key.substring(0, dotIdx);
const variant = key.substring(dotIdx + 1);
const baseInfo = KNOWN_SECRETS[baseKey];
if (baseInfo) label = chalk.dim(` (${baseInfo.label} · ${variant})`);
}
}
console.log(` ${chalk.cyan(key)} = ${masked}${label}`);
}
console.log(chalk.dim(`\n File: ${SECRETS_FILE}\n`));
});
cmd.addCommand(list);

// ─── remove <key> ──────────────────────────────────────────────────────────
const remove = new Command("remove");
remove
.description("Remove a stored secret")
.argument("<key>", "Secret key to remove")
.action((key: string) => {
if (deleteSecret(key)) {
console.log(chalk.green(` ✔ Removed '${key}'`));
} else {
console.log(chalk.yellow(` '${key}' not found in secrets`));
}
});
cmd.addCommand(remove);

// Subcommand: update credentials for a running AKS sandbox
const update = new Command("update");
update
Expand Down
Loading
Loading