Skip to content
This repository was archived by the owner on Jun 9, 2026. It is now read-only.

Commit c8a8bae

Browse files
committed
fix(plugins): round-5 CodeRabbit hardening
- session-registry: wrap readRegistry calls in try-catch in sessionRegistryTool, sessionResultsTool, and buildRegistryContext to handle I/O errors gracefully (EACCES etc.) - session-registry: normalizeRegistry status check now validates membership in ['running','completed','failed'] enum instead of typeof === 'string' - voice-notification: add path traversal guard in getActiveWorkDir() using resolve() + startsWith(workRoot) check - voice-notification: extractVoiceCompletion regex now Unicode-aware ([\p{L}\p{M}\w\s\-.']+ with /u flag) to support non-ASCII names
1 parent 63296ba commit c8a8bae

2 files changed

Lines changed: 33 additions & 9 deletions

File tree

‎.opencode/plugins/handlers/session-registry.ts‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ function normalizeRegistry(data: unknown, sessionId: string): SubagentRegistry {
6060
typeof (e as Record<string, unknown>).agentType === "string" &&
6161
typeof (e as Record<string, unknown>).description === "string" &&
6262
typeof (e as Record<string, unknown>).spawnedAt === "string" &&
63-
typeof (e as Record<string, unknown>).status === "string"
63+
(["running", "completed", "failed"] as const).includes(
64+
(e as Record<string, unknown>).status as SubagentEntry["status"]
65+
)
6466
)
6567
: [],
6668
updatedAt:
@@ -298,7 +300,13 @@ export const sessionRegistryTool = tool({
298300
"The results are always available — subagent data survives compaction.",
299301
args: {},
300302
async execute(_args: {}, context: ToolContext): Promise<string> {
301-
const registry = readRegistry(context.sessionID);
303+
let registry: SubagentRegistry;
304+
try {
305+
registry = readRegistry(context.sessionID);
306+
} catch (err) {
307+
fileLog(`[session-registry] sessionRegistryTool: failed to read registry: ${err}`, "error");
308+
return "Registry unavailable: could not read session data (I/O error). Check file permissions on the state directory.";
309+
}
302310

303311
if (registry.entries.length === 0) {
304312
return "No subagent sessions found for this session. No subagents have been spawned via the Task tool yet.";
@@ -350,7 +358,13 @@ export const sessionResultsTool = tool({
350358
},
351359
async execute(args: { session_id: string }, context: ToolContext): Promise<string> {
352360
// Read the registry file to get stored metadata for this session
353-
const registry = readRegistry(context.sessionID);
361+
let registry: SubagentRegistry;
362+
try {
363+
registry = readRegistry(context.sessionID);
364+
} catch (err) {
365+
fileLog(`[session-registry] sessionResultsTool: failed to read registry: ${err}`, "error");
366+
return "Registry unavailable: could not read session data (I/O error). Check file permissions on the state directory.";
367+
}
354368
const entry = registry.entries.find((e) => e.sessionId === args.session_id);
355369

356370
if (!entry) {
@@ -380,7 +394,13 @@ export const sessionResultsTool = tool({
380394
* Called by WP-N2 compaction intelligence handler.
381395
*/
382396
export function buildRegistryContext(sessionId: string): string | null {
383-
const registry = readRegistry(sessionId);
397+
let registry: SubagentRegistry;
398+
try {
399+
registry = readRegistry(sessionId);
400+
} catch (err) {
401+
fileLog(`[session-registry] buildRegistryContext: failed to read registry: ${err}`, "error");
402+
return null;
403+
}
384404
if (registry.entries.length === 0) return null;
385405

386406
const lines = [

‎.opencode/plugins/handlers/voice-notification.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
unlinkSync,
2525
writeFileSync,
2626
} from "node:fs";
27-
import { join } from "node:path";
27+
import { join, resolve } from "node:path";
2828
import { promisify } from "node:util";
2929
import { fileLog } from "../lib/file-logger";
3030
import { getIdentity, getSettings } from "../lib/identity";
@@ -88,8 +88,12 @@ function getActiveWorkDir(): string | null {
8888
const content = readFileSync(currentWorkPath, "utf-8");
8989
const state = JSON.parse(content);
9090
if (state.work_dir) {
91-
const workPath = join(getWorkDir(), state.work_dir);
92-
if (existsSync(workPath)) return workPath;
91+
const workRoot = resolve(getWorkDir());
92+
const workPath = resolve(join(getWorkDir(), state.work_dir));
93+
// Guard against path traversal (e.g. work_dir = "../../etc")
94+
if (workPath.startsWith(workRoot + "/") || workPath === workRoot) {
95+
if (existsSync(workPath)) return workPath;
96+
}
9397
}
9498
} catch {
9599
// Silent fail
@@ -457,8 +461,8 @@ export async function handleVoiceNotification(
457461
export function extractVoiceCompletion(text: string): string | null {
458462
if (!text) return null;
459463

460-
// Pattern: 🗣️ Name: message
461-
const match = text.match(/🗣️\s*[\w\s]+:\s*(.+?)(?:\n|$)/);
464+
// Pattern: 🗣️ Name: message (Unicode-aware to support non-ASCII names like "Ava", accented chars)
465+
const match = text.match(/🗣️\s*[\p{L}\p{M}\w\s\-.']+:\s*(.+?)(?:\n|$)/u);
462466
if (match) {
463467
return match[1].trim();
464468
}

0 commit comments

Comments
 (0)