Add OS-aware runtime context for cross-platform shell execution

Detect the runtime platform and default shell at startup, inject
platform context into assistant instructions, and replace hardcoded
/bin/sh with the detected shell in command executors (cli + electron).

Made-with: Cursor
This commit is contained in:
tusharmagar 2026-02-26 11:42:43 +05:30
parent a3e74931bf
commit b5424d92f9
6 changed files with 162 additions and 8 deletions

View file

@ -1,5 +1,8 @@
import { skillCatalog } from "./skills/index.js";
import { WorkDir as BASE_DIR } from "../../config/config.js";
import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js";
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
export const CopilotInstructions = `You are an intelligent workflow assistant helping users manage their workflows in ${BASE_DIR}. You can also help the user with general tasks.
@ -39,6 +42,8 @@ When a user asks for ANY task that might require external capabilities (web sear
- Use relative paths (no \${BASE_DIR} prefixes) when running commands or referencing files.
- Keep user data safedouble-check before editing or deleting important resources.
${runtimeContextPrompt}
## Workspace access & scope
- You have full read/write access inside \`${BASE_DIR}\` (this resolves to the user's \`~/.rowboat\` directory). Create folders, files, and agents there using builtin tools or allowed shell commands—don't wait for the user to do it manually.
- If a user mentions a different root (e.g., \`~/.rowboatx\` or another path), clarify whether they meant the Rowboat workspace and propose the equivalent path you can act on. Only refuse if they explicitly insist on an inaccessible location.

View file

@ -0,0 +1,69 @@
export type RuntimeShellDialect = 'windows-cmd' | 'posix-sh';
export type RuntimeOsName = 'Windows' | 'macOS' | 'Linux' | 'Unknown';
export interface RuntimeContext {
platform: NodeJS.Platform;
osName: RuntimeOsName;
shellDialect: RuntimeShellDialect;
shellExecutable: string;
}
export function getExecutionShell(platform: NodeJS.Platform = process.platform): string {
return platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh';
}
export function getRuntimeContext(platform: NodeJS.Platform = process.platform): RuntimeContext {
if (platform === 'win32') {
return {
platform,
osName: 'Windows',
shellDialect: 'windows-cmd',
shellExecutable: getExecutionShell(platform),
};
}
if (platform === 'darwin') {
return {
platform,
osName: 'macOS',
shellDialect: 'posix-sh',
shellExecutable: getExecutionShell(platform),
};
}
if (platform === 'linux') {
return {
platform,
osName: 'Linux',
shellDialect: 'posix-sh',
shellExecutable: getExecutionShell(platform),
};
}
return {
platform,
osName: 'Unknown',
shellDialect: 'posix-sh',
shellExecutable: getExecutionShell(platform),
};
}
export function getRuntimeContextPrompt(runtime: RuntimeContext): string {
if (runtime.shellDialect === 'windows-cmd') {
return `## Runtime Platform (CRITICAL)
- Detected platform: **${runtime.platform}**
- Detected OS: **${runtime.osName}**
- Shell used by executeCommand: **${runtime.shellExecutable}** (Windows Command Prompt / cmd syntax)
- Use Windows command syntax for executeCommand (for example: \`dir\`, \`type\`, \`copy\`, \`move\`, \`del\`, \`rmdir\`).
- Use Windows-style absolute paths when outside workspace (for example: \`C:\\Users\\...\`).
- Do not assume macOS/Linux command syntax when the runtime is Windows.`;
}
return `## Runtime Platform (CRITICAL)
- Detected platform: **${runtime.platform}**
- Detected OS: **${runtime.osName}**
- Shell used by executeCommand: **${runtime.shellExecutable}** (POSIX sh syntax)
- Use POSIX command syntax for executeCommand (for example: \`ls\`, \`cat\`, \`cp\`, \`mv\`, \`rm\`).
- Use POSIX paths when outside workspace (for example: \`~/Desktop\`, \`/Users/.../\` on macOS, \`/home/.../\` on Linux).
- Do not assume Windows command syntax when the runtime is POSIX.`;
}

View file

@ -1,11 +1,13 @@
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
import { getSecurityAllowList, SECURITY_CONFIG_PATH } from '../../config/security.js';
import { getExecutionShell } from '../assistant/runtime-context.js';
const execPromise = promisify(exec);
const COMMAND_SPLIT_REGEX = /(?:\|\||&&|;|\||\n)/;
const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
const WRAPPER_COMMANDS = new Set(['sudo', 'env', 'time', 'command']);
const EXECUTION_SHELL = getExecutionShell();
function sanitizeToken(token: string): string {
return token.trim().replace(/^['"]+|['"]+$/g, '');
@ -91,7 +93,7 @@ export async function executeCommand(
cwd: options?.cwd,
timeout: options?.timeout,
maxBuffer: options?.maxBuffer || 1024 * 1024, // default 1MB
shell: '/bin/sh', // use sh for cross-platform compatibility
shell: EXECUTION_SHELL,
});
return {
@ -125,7 +127,7 @@ export function executeCommandSync(
cwd: options?.cwd,
timeout: options?.timeout,
encoding: 'utf-8',
shell: '/bin/sh',
shell: EXECUTION_SHELL,
});
return {