mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-03 12:22:38 +02:00
prefix based allowlist
This commit is contained in:
parent
4fb153f5dc
commit
6fa7fdc4df
2 changed files with 63 additions and 20 deletions
|
|
@ -1961,6 +1961,11 @@ function App() {
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Auto-collapse tool after completion if it was auto-opened for streaming
|
||||||
|
if (event.toolCallId) {
|
||||||
|
setToolOpenForTab(activeChatTabIdRef.current, event.toolCallId, false)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle app-navigation tool results — trigger UI side effects
|
// Handle app-navigation tool results — trigger UI side effects
|
||||||
if (event.toolName === 'app-navigation') {
|
if (event.toolName === 'app-navigation') {
|
||||||
const result = event.result as { success?: boolean; action?: string; [key: string]: unknown } | undefined
|
const result = event.result as { success?: boolean; action?: string; [key: string]: unknown } | undefined
|
||||||
|
|
@ -1980,6 +1985,10 @@ function App() {
|
||||||
isToolCall(item)
|
isToolCall(item)
|
||||||
&& item.id === event.toolCallId
|
&& item.id === event.toolCallId
|
||||||
) {
|
) {
|
||||||
|
// Auto-open the tool collapsible on first streaming chunk
|
||||||
|
if (!item.streamingOutput) {
|
||||||
|
setToolOpenForTab(activeChatTabIdRef.current, item.id, true)
|
||||||
|
}
|
||||||
return { ...item, streamingOutput: (item.streamingOutput ?? '') + event.output }
|
return { ...item, streamingOutput: (item.streamingOutput ?? '') + event.output }
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
|
|
|
||||||
|
|
@ -14,48 +14,82 @@ function sanitizeToken(token: string): string {
|
||||||
return token.trim().replace(/^['"()]+|['"()]+$/g, '');
|
return token.trim().replace(/^['"()]+|['"()]+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractCommandNames(command: string): string[] {
|
/**
|
||||||
const discovered = new Set<string>();
|
* Extract command-name segments from a shell command string.
|
||||||
const segments = command.split(COMMAND_SPLIT_REGEX);
|
* Each segment is the leading tokens (after env-var assignments) of a
|
||||||
|
* pipeline / chain element, joined by spaces and lowercased.
|
||||||
|
* e.g. "cd ~/foo && npx acpx@latest claude exec 'hello'" → ["cd", "npx acpx@latest claude exec"]
|
||||||
|
*/
|
||||||
|
function extractCommandSegments(command: string): string[] {
|
||||||
|
const segments: string[] = [];
|
||||||
|
const stripped = command.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '""');
|
||||||
|
const parts = stripped.split(COMMAND_SPLIT_REGEX);
|
||||||
|
|
||||||
for (const segment of segments) {
|
for (const part of parts) {
|
||||||
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
const tokens = part.trim().split(/\s+/).filter(Boolean);
|
||||||
if (!tokens.length) continue;
|
if (!tokens.length) continue;
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < tokens.length && ENV_ASSIGNMENT_REGEX.test(tokens[index])) {
|
while (index < tokens.length && ENV_ASSIGNMENT_REGEX.test(tokens[index])) {
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= tokens.length) continue;
|
if (index >= tokens.length) continue;
|
||||||
|
|
||||||
const primary = sanitizeToken(tokens[index]).toLowerCase();
|
const cmdTokens = tokens.slice(index).map(t => sanitizeToken(t).toLowerCase()).filter(Boolean);
|
||||||
if (!primary) continue;
|
if (cmdTokens.length) {
|
||||||
|
segments.push(cmdTokens.join(' '));
|
||||||
discovered.add(primary);
|
|
||||||
|
|
||||||
if (WRAPPER_COMMANDS.has(primary) && index + 1 < tokens.length) {
|
|
||||||
const wrapped = sanitizeToken(tokens[index + 1]).toLowerCase();
|
|
||||||
if (wrapped) {
|
|
||||||
discovered.add(wrapped);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractCommandNames(command: string): string[] {
|
||||||
|
const discovered = new Set<string>();
|
||||||
|
for (const segment of extractCommandSegments(command)) {
|
||||||
|
const first = segment.split(/\s+/)[0];
|
||||||
|
if (first) discovered.add(first);
|
||||||
|
}
|
||||||
return Array.from(discovered);
|
return Array.from(discovered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix-based allow check: a command segment is allowed if any allowlist
|
||||||
|
* entry is a prefix of it. e.g. allowlist "npx" allows "npx acpx@latest ...",
|
||||||
|
* and allowlist "npx acpx@latest" also allows "npx acpx@latest claude exec ...".
|
||||||
|
*/
|
||||||
|
function isSegmentAllowed(segment: string, allowEntries: string[]): boolean {
|
||||||
|
for (const entry of allowEntries) {
|
||||||
|
// entry is a prefix of the segment (e.g. "npx" matches "npx acpx@latest claude exec")
|
||||||
|
if (segment === entry || segment.startsWith(entry + ' ')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function findBlockedCommands(command: string, sessionAllowedCommands?: Set<string>): string[] {
|
function findBlockedCommands(command: string, sessionAllowedCommands?: Set<string>): string[] {
|
||||||
const invoked = extractCommandNames(command);
|
const segments = extractCommandSegments(command);
|
||||||
if (!invoked.length) return [];
|
if (!segments.length) return [];
|
||||||
|
|
||||||
const allowList = getSecurityAllowList();
|
const allowList = getSecurityAllowList();
|
||||||
if (!allowList.length && (!sessionAllowedCommands || sessionAllowedCommands.size === 0)) return invoked;
|
if (!allowList.length && (!sessionAllowedCommands || sessionAllowedCommands.size === 0)) {
|
||||||
|
return segments.map(s => s.split(/\s+/)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
const allowSet = new Set(allowList);
|
const allowSet = new Set(allowList);
|
||||||
if (allowSet.has('*')) return [];
|
if (allowSet.has('*')) return [];
|
||||||
|
|
||||||
return invoked.filter((cmd) => !allowSet.has(cmd) && !sessionAllowedCommands?.has(cmd));
|
const allEntries = [...allowSet, ...(sessionAllowedCommands ?? [])];
|
||||||
|
|
||||||
|
const blocked: string[] = [];
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (!isSegmentAllowed(segment, allEntries)) {
|
||||||
|
const first = segment.split(/\s+/)[0];
|
||||||
|
if (first) blocked.push(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBlocked(command: string, sessionAllowedCommands?: Set<string>): boolean {
|
export function isBlocked(command: string, sessionAllowedCommands?: Set<string>): boolean {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue