mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-08-15 16:00:29 +02:00
feat(worker): enforce bounded bash timeouts via pi extension
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* pi extension: enforce a bounded timeout on every `bash` tool call.
|
||||||
|
*
|
||||||
|
* pi's built-in bash tool accepts an optional `timeout` (in seconds) but applies
|
||||||
|
* NO default and NO upper bound — an unbounded command (e.g. a `playwright-cli`
|
||||||
|
* browser action that never returns) hangs the agent indefinitely. This extension
|
||||||
|
* registers a `tool_call` pre-execution handler that blocks any `bash` invocation
|
||||||
|
* that omits `timeout` or sets it above the maximum, returning a message that tells
|
||||||
|
* the model how to re-run the command correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ExtensionAPI, ToolCallEvent, ToolCallEventResult } from '@earendil-works/pi-coding-agent';
|
||||||
|
import { isToolCallEventType } from '@earendil-works/pi-coding-agent';
|
||||||
|
|
||||||
|
/** Recommended timeout (seconds) suggested to the model when it omits one. */
|
||||||
|
const DEFAULT_TIMEOUT_SECONDS = 120;
|
||||||
|
|
||||||
|
/** Hard upper bound (seconds) a single bash command may run. */
|
||||||
|
const MAX_TIMEOUT_SECONDS = 600;
|
||||||
|
|
||||||
|
function evaluateBashTimeout(timeout: number | undefined): ToolCallEventResult | undefined {
|
||||||
|
const hasValidTimeout = typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0;
|
||||||
|
if (!hasValidTimeout) {
|
||||||
|
return {
|
||||||
|
block: true,
|
||||||
|
reason: `Set bash 'timeout' (seconds). Default ${DEFAULT_TIMEOUT_SECONDS}s, max ${MAX_TIMEOUT_SECONDS}s.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout > MAX_TIMEOUT_SECONDS) {
|
||||||
|
return {
|
||||||
|
block: true,
|
||||||
|
reason: `bash 'timeout' ${timeout}s exceeds max ${MAX_TIMEOUT_SECONDS}s. Default ${DEFAULT_TIMEOUT_SECONDS}s, max ${MAX_TIMEOUT_SECONDS}s.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function bashTimeoutExtension(pi: ExtensionAPI): void {
|
||||||
|
pi.on('tool_call', (event: ToolCallEvent): ToolCallEventResult | undefined => {
|
||||||
|
if (!isToolCallEventType('bash', event)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return evaluateBashTimeout(event.input.timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from '@earendil-works/pi-coding-agent';
|
} from '@earendil-works/pi-coding-agent';
|
||||||
import { fs, path } from 'zx';
|
import { fs, path } from 'zx';
|
||||||
import type { AuditSession } from '../audit/index.js';
|
import type { AuditSession } from '../audit/index.js';
|
||||||
import { deliverablesDir, PLAYWRIGHT_SKILL_DIR } from '../paths.js';
|
import { BASH_TIMEOUT_EXTENSION_DIR, deliverablesDir, PLAYWRIGHT_SKILL_DIR } from '../paths.js';
|
||||||
import { isRetryableError, PentestError } from '../services/error-handling.js';
|
import { isRetryableError, PentestError } from '../services/error-handling.js';
|
||||||
import { AGENT_VALIDATORS } from '../session-manager.js';
|
import { AGENT_VALIDATORS } from '../session-manager.js';
|
||||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||||
@@ -65,7 +65,8 @@ function permissionExtensionDir(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildResourceLoader(cwd: string, logger: ActivityLogger): Promise<ResourceLoader> {
|
async function buildResourceLoader(cwd: string, logger: ActivityLogger): Promise<ResourceLoader> {
|
||||||
const additionalExtensionPaths: string[] = [];
|
// Always enforce bounded bash timeouts so an unbounded command cannot hang the agent.
|
||||||
|
const additionalExtensionPaths: string[] = [BASH_TIMEOUT_EXTENSION_DIR];
|
||||||
if (fs.existsSync(permissionConfigPath())) {
|
if (fs.existsSync(permissionConfigPath())) {
|
||||||
const extDir = permissionExtensionDir();
|
const extDir = permissionExtensionDir();
|
||||||
if (extDir) {
|
if (extDir) {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export const CONFIGS_DIR = path.join(WORKER_ROOT, 'configs');
|
|||||||
|
|
||||||
export const PLAYWRIGHT_SKILL_DIR = path.join(os.homedir(), '.claude', 'skills', 'playwright-cli');
|
export const PLAYWRIGHT_SKILL_DIR = path.join(os.homedir(), '.claude', 'skills', 'playwright-cli');
|
||||||
|
|
||||||
|
/** Compiled pi extension dir that enforces bounded `bash` timeouts (resolved from dist/) */
|
||||||
|
export const BASH_TIMEOUT_EXTENSION_DIR = path.join(import.meta.dirname, 'ai', 'extensions', 'bash-timeout');
|
||||||
|
|
||||||
/** Default deliverables subdirectory relative to repoPath */
|
/** Default deliverables subdirectory relative to repoPath */
|
||||||
export const DEFAULT_DELIVERABLES_SUBDIR = '.shannon/deliverables';
|
export const DEFAULT_DELIVERABLES_SUBDIR = '.shannon/deliverables';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user