mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-07 06:12:44 +02:00
Add security allowlist for command execution and update copilot instructions
- Add security.ts with allowlist configuration for shell commands - Update command-executor.ts to enforce security policy (exit code 126 for blocked commands) - Update copilot instructions to clarify builtin tools vs shell commands - Document that builtin tools (deleteFile, createFile, etc.) bypass security filtering - Only executeCommand (shell commands) requires security.json allowlist entries
This commit is contained in:
parent
570543e1c7
commit
28488d5fd1
4 changed files with 183 additions and 1 deletions
90
apps/cli/src/application/config/security.ts
Normal file
90
apps/cli/src/application/config/security.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { WorkDir } from "./config.js";
|
||||
|
||||
export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json");
|
||||
|
||||
const DEFAULT_ALLOW_LIST = ["ls", "pwd", "cat", "echo", "whoami"];
|
||||
|
||||
let cachedAllowList: string[] | null = null;
|
||||
let cachedMtimeMs: number | null = null;
|
||||
|
||||
function ensureSecurityConfig() {
|
||||
if (!fs.existsSync(SECURITY_CONFIG_PATH)) {
|
||||
fs.writeFileSync(
|
||||
SECURITY_CONFIG_PATH,
|
||||
JSON.stringify(DEFAULT_ALLOW_LIST, null, 2) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeList(commands: unknown[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
for (const entry of commands) {
|
||||
if (typeof entry !== "string") continue;
|
||||
const normalized = entry.trim().toLowerCase();
|
||||
if (!normalized) continue;
|
||||
seen.add(normalized);
|
||||
}
|
||||
|
||||
return Array.from(seen);
|
||||
}
|
||||
|
||||
function parseSecurityPayload(payload: unknown): string[] {
|
||||
if (Array.isArray(payload)) {
|
||||
return normalizeList(payload);
|
||||
}
|
||||
|
||||
if (payload && typeof payload === "object") {
|
||||
const maybeObject = payload as Record<string, unknown>;
|
||||
if (Array.isArray(maybeObject.allowedCommands)) {
|
||||
return normalizeList(maybeObject.allowedCommands);
|
||||
}
|
||||
|
||||
const dynamicList = Object.entries(maybeObject)
|
||||
.filter(([, value]) => Boolean(value))
|
||||
.map(([key]) => key);
|
||||
|
||||
return normalizeList(dynamicList);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function readAllowList(): string[] {
|
||||
ensureSecurityConfig();
|
||||
|
||||
try {
|
||||
const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8");
|
||||
const parsed = JSON.parse(configContent);
|
||||
return parseSecurityPayload(parsed);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read security config at ${SECURITY_CONFIG_PATH}: ${error instanceof Error ? error.message : error}`);
|
||||
return DEFAULT_ALLOW_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSecurityAllowList(): string[] {
|
||||
ensureSecurityConfig();
|
||||
try {
|
||||
const stats = fs.statSync(SECURITY_CONFIG_PATH);
|
||||
if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) {
|
||||
return cachedAllowList;
|
||||
}
|
||||
|
||||
const allowList = readAllowList();
|
||||
cachedAllowList = allowList;
|
||||
cachedMtimeMs = stats.mtimeMs;
|
||||
return allowList;
|
||||
} catch {
|
||||
cachedAllowList = null;
|
||||
cachedMtimeMs = null;
|
||||
return readAllowList();
|
||||
}
|
||||
}
|
||||
|
||||
export function resetSecurityAllowListCache() {
|
||||
cachedAllowList = null;
|
||||
cachedMtimeMs = null;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue