Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
90 changes: 90 additions & 0 deletions extensions/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

type Locale = "en" | "es" | "fr" | "pt-BR";
type Params = Record<string, string | number>;

const translations: Record<Exclude<Locale, "en">, Record<string, string>> = {
es: {
"security.mode.current": "Modo actual: {mode}",
"security.config.path": "Ruta de configuración: {path}",
"security.mode.basic": "Básico: comandos críticos bloqueados, localhost/127.x permitido",
"security.mode.max": "Máximo: todos los comandos bloqueados, protección SSRF completa",
"security.mode.setBasic": "/security mode basic — relajar restricciones para desarrollo",
"security.mode.setMax": "/security mode max — bloqueo completo (predeterminado)",
"security.alreadyMode": "El modo de seguridad ya es {mode}",
"security.persistFailed": "ERROR al guardar el modo de seguridad: no se pudo escribir {path}",
"security.modeChanged": "Modo de seguridad cambiado a {mode}",
"security.invalidMode": "Modo no válido: \"{mode}\". Usa \"basic\" o \"max\".",
"security.extendedAllowed": "Los comandos extendidos ahora están PERMITIDOS: rm, sudo, npm, apt, git, curl, wget, etc.",
"security.localhostAllowed": "Las URLs localhost y 127.x ahora están PERMITIDAS para SSRF",
"security.criticalStillBlocked": "Los comandos críticos siguen bloqueados: dd, mkfs, shred, fdisk, ssh, etc.",
"security.fullLockdown": "Bloqueo completo activo — los {count} comandos están bloqueados",
"security.fullSsrf": "Protección SSRF completa — localhost e IPs privadas bloqueadas",
"security.auditRequiresTui": "La auditoría de seguridad requiere modo TUI",
},
fr: {
"security.mode.current": "Mode actuel : {mode}",
"security.config.path": "Chemin de configuration : {path}",
"security.mode.basic": "Basique : commandes critiques bloquées, localhost/127.x autorisé",
"security.mode.max": "Maximum : toutes les commandes bloquées, protection SSRF complète",
"security.mode.setBasic": "/security mode basic — assouplir les restrictions pour le développement",
"security.mode.setMax": "/security mode max — verrouillage complet (par défaut)",
"security.alreadyMode": "Le mode de sécurité est déjà {mode}",
"security.persistFailed": "ÉCHEC de l’enregistrement du mode de sécurité : impossible d’écrire {path}",
"security.modeChanged": "Mode de sécurité défini sur {mode}",
"security.invalidMode": "Mode non valide : \"{mode}\". Utilisez \"basic\" ou \"max\".",
"security.extendedAllowed": "Les commandes étendues sont maintenant AUTORISÉES : rm, sudo, npm, apt, git, curl, wget, etc.",
"security.localhostAllowed": "Les URL localhost et 127.x sont maintenant AUTORISÉES pour SSRF",
"security.criticalStillBlocked": "Les commandes critiques restent bloquées : dd, mkfs, shred, fdisk, ssh, etc.",
"security.fullLockdown": "Verrouillage complet actif — les {count} commandes sont bloquées",
"security.fullSsrf": "Protection SSRF complète — localhost et IP privées bloqués",
"security.auditRequiresTui": "L’audit de sécurité nécessite le mode TUI",
},
"pt-BR": {
"security.mode.current": "Modo atual: {mode}",
"security.config.path": "Caminho de configuração: {path}",
"security.mode.basic": "Básico: comandos críticos bloqueados, localhost/127.x permitido",
"security.mode.max": "Máximo: todos os comandos bloqueados, proteção SSRF completa",
"security.mode.setBasic": "/security mode basic — relaxar restrições para desenvolvimento",
"security.mode.setMax": "/security mode max — bloqueio completo (padrão)",
"security.alreadyMode": "O modo de segurança já é {mode}",
"security.persistFailed": "FALHA ao persistir o modo de segurança: não foi possível escrever {path}",
"security.modeChanged": "Modo de segurança definido para {mode}",
"security.invalidMode": "Modo inválido: \"{mode}\". Use \"basic\" ou \"max\".",
"security.extendedAllowed": "Comandos estendidos agora estão PERMITIDOS: rm, sudo, npm, apt, git, curl, wget, etc.",
"security.localhostAllowed": "URLs localhost e 127.x agora estão PERMITIDAS para SSRF",
"security.criticalStillBlocked": "Comandos críticos continuam bloqueados: dd, mkfs, shred, fdisk, ssh, etc.",
"security.fullLockdown": "Bloqueio completo ativo — todos os {count} comandos bloqueados",
"security.fullSsrf": "Proteção SSRF completa — localhost e IPs privados bloqueados",
"security.auditRequiresTui": "A auditoria de segurança requer modo TUI",
},
};

let currentLocale: Locale = "en";

export function initI18n(pi: ExtensionAPI): void {
pi.events?.emit?.("pi-core/i18n/registerBundle", {
namespace: "vtstech-security",
defaultLocale: "en",
locales: translations,
});

pi.events?.emit?.("pi-core/i18n/requestApi", {
onReady: (api: { getLocale?: () => string; onLocaleChange?: (cb: (locale: string) => void) => void }) => {
const next = api.getLocale?.();
if (isLocale(next)) currentLocale = next;
api.onLocaleChange?.((locale) => {
if (isLocale(locale)) currentLocale = locale;
});
},
});
}

export function t(key: string, fallback: string, params: Params = {}): string {
const template = currentLocale === "en" ? fallback : translations[currentLocale]?.[key] ?? fallback;
return template.replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`));
}

function isLocale(locale: string | undefined): locale is Locale {
return locale === "en" || locale === "es" || locale === "fr" || locale === "pt-BR";
}
36 changes: 19 additions & 17 deletions extensions/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import { debugLog } from "../shared/debug";
import { section, ok, fail, warn, info } from "../shared/format";
import { EXTENSION_VERSION } from "../shared/ollama";
import { initI18n, t } from "./i18n";

// ── Types ────────────────────────────────────────────────────────────────

Expand All @@ -54,6 +55,7 @@ interface SecurityStats {
// ── Extension ────────────────────────────────────────────────────────────

export default function (pi: ExtensionAPI) {
initI18n(pi);
const stats: SecurityStats = {
blocked: 0,
allowed: 0,
Expand Down Expand Up @@ -92,19 +94,19 @@ export default function (pi: ExtensionAPI) {
if (!value) {
const lines: string[] = [branding];
lines.push(section("SECURITY MODE"));
lines.push(info(`Current mode: ${currentMode.toUpperCase()}`));
lines.push(info(`Config path: ${SECURITY_CONFIG_PATH}`));
lines.push(info(t("security.mode.current", "Current mode: {mode}", { mode: currentMode.toUpperCase() })));
lines.push(info(t("security.config.path", "Config path: {path}", { path: SECURITY_CONFIG_PATH })));
lines.push(info(`Critical commands (always blocked): ${CRITICAL_COMMANDS.size}`));
lines.push(info(`Extended commands (max only): ${EXTENDED_COMMANDS.size}`));
lines.push(info(`Total blocked (max): ${CRITICAL_COMMANDS.size + EXTENDED_COMMANDS.size}`));
lines.push(info(`URL patterns always blocked: ${BLOCKED_URL_ALWAYS.size}`));
lines.push(info(`URL patterns (max only): ${BLOCKED_URL_MAX_ONLY.size}`));
lines.push(section("MODE DIFFERENCES"));
lines.push(info("Basic: critical commands blocked, localhost/127.x allowed"));
lines.push(info("Max: all commands blocked, full SSRF protection"));
lines.push(info(t("security.mode.basic", "Basic: critical commands blocked, localhost/127.x allowed")));
lines.push(info(t("security.mode.max", "Max: all commands blocked, full SSRF protection")));
lines.push(section("SWITCH MODE"));
lines.push(info("/security mode basic — relax restrictions for development"));
lines.push(info("/security mode max — full lockdown (default)"));
lines.push(info(t("security.mode.setBasic", "/security mode basic — relax restrictions for development")));
lines.push(info(t("security.mode.setMax", "/security mode max — full lockdown (default)")));
lines.push(branding);

pi.sendMessage({
Expand All @@ -118,19 +120,19 @@ export default function (pi: ExtensionAPI) {
// /security mode basic|max
if (value === "basic" || value === "max") {
if (value === currentMode) {
ctx.ui.notify(`Security mode is already ${value.toUpperCase()}`, "info");
ctx.ui.notify(t("security.alreadyMode", "Security mode is already {mode}", { mode: value.toUpperCase() }), "info");
return;
}

const writeOk = setSecurityMode(value as "basic" | "max");
if (!writeOk) {
ctx.ui.notify(`FAILED to persist security mode: could not write ${SECURITY_CONFIG_PATH}`, "error");
ctx.ui.notify(t("security.persistFailed", "FAILED to persist security mode: could not write {path}", { path: SECURITY_CONFIG_PATH }), "error");
debugLog("security", `/security mode ${value}: write failed`, { path: SECURITY_CONFIG_PATH });
return;
}

ctx.ui.setStatus("status-sec", value.toUpperCase());
ctx.ui.notify(`Security mode set to ${value.toUpperCase()}`, "success");
ctx.ui.notify(t("security.modeChanged", "Security mode set to {mode}", { mode: value.toUpperCase() }), "success");

appendAuditEntry({
timestamp: new Date().toISOString(),
Expand All @@ -149,12 +151,12 @@ export default function (pi: ExtensionAPI) {
lines.push(info(`Config: ${SECURITY_CONFIG_PATH}`));

if (value === "basic") {
lines.push(warn("Extended commands are now ALLOWED: rm, sudo, npm, apt, git, curl, wget, etc."));
lines.push(warn("Localhost and 127.x URLs are now ALLOWED for SSRF"));
lines.push(ok("Critical commands remain blocked: dd, mkfs, shred, fdisk, ssh, etc."));
lines.push(warn(t("security.extendedAllowed", "Extended commands are now ALLOWED: rm, sudo, npm, apt, git, curl, wget, etc.")));
lines.push(warn(t("security.localhostAllowed", "Localhost and 127.x URLs are now ALLOWED for SSRF")));
lines.push(ok(t("security.criticalStillBlocked", "Critical commands remain blocked: dd, mkfs, shred, fdisk, ssh, etc.")));
} else {
lines.push(ok(`Full lockdown active — all ${totalCmds} commands blocked`));
lines.push(ok("Full SSRF protection — localhost and private IPs blocked"));
lines.push(ok(t("security.fullLockdown", "Full lockdown active — all {count} commands blocked", { count: totalCmds })));
lines.push(ok(t("security.fullSsrf", "Full SSRF protection — localhost and private IPs blocked")));
}

lines.push(branding);
Expand All @@ -168,7 +170,7 @@ export default function (pi: ExtensionAPI) {
}

// Invalid mode value
ctx.ui.notify(`Invalid mode: "${value}". Use \"basic\" or \"max\".`, "error");
ctx.ui.notify(t("security.invalidMode", "Invalid mode: \"{mode}\". Use \"basic\" or \"max\".", { mode: value || "" }), "error");
return;
}

Expand Down Expand Up @@ -342,7 +344,7 @@ export default function (pi: ExtensionAPI) {

// Security mode
lines.push(section("SECURITY MODE"));
lines.push(info(`Current mode: ${currentMode.toUpperCase()}`));
lines.push(info(t("security.mode.current", "Current mode: {mode}", { mode: currentMode.toUpperCase() })));
lines.push(info(`Config file: ${SECURITY_CONFIG_PATH}`));

// Effective blocklist summary
Expand Down Expand Up @@ -428,7 +430,7 @@ export default function (pi: ExtensionAPI) {
description: "Show security audit report — blocked operations, stats, and recent log",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("Security audit requires TUI mode", "error");
ctx.ui.notify(t("security.auditRequiresTui", "Security audit requires TUI mode"), "error");
return;
}
try {
Expand Down