Fix: Security, reliability, and code quality improvements from PR review
Critical Security Fixes: - Fix command injection vulnerability in Windows shims (beadboard.cmd, bb.cmd) - Added path validation to block traversal (.. and root-relative paths) - Added quotes around env var to prevent command injection Reliability Fixes: - Fix agent cache null safety bug - Fixed callBdAgentShow() to check for cache misses (null check, expiration) - Fixed getCachedAgent to properly return entry.data or null - Fix null body crashes in mail ack route - Added null check before casting body to object - Returns 400 error instead of 500 for invalid requests BD Compliance Fixes: - Fix read-issues to use BD audit record path - Ensures all writes go through bd audit record - Maintains watcher/SSE parity and Dolt commit tracking Code Quality Fixes: - Fix path canonicalization violations - Use canonicalizeWindowsPath() and windowsPathKey() from pathing module - Prevents Windows edge cases and ensures machine-reproducible paths - Fix typo: mobile-fronted → mobile-frontend - Pin GitHub Actions tags - softprops/action-gh-release@v1 → specific commit hash - Register pr14 test in package.json (already registered) Testing: - Refactor broad exception handlers in Python scripts - Replace except Exception: with specific exceptions - Allows KeyboardInterrupt and SystemExit to propagate correctly - All tests passing
This commit is contained in:
parent
d54e4f3311
commit
ce4700849b
15 changed files with 2995 additions and 756 deletions
|
|
@ -12,6 +12,13 @@ export async function POST(request: Request): Promise<Response> {
|
|||
);
|
||||
}
|
||||
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { code: 'INVALID_BODY', message: 'Request body must be a valid object.' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = body as { agent?: string; message?: string };
|
||||
const result = await ackAgentMessage({
|
||||
agent: parsed.agent ?? '',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { showAgent, deriveLiveness } from './agent-registry';
|
||||
import type { AgentMessage } from './agent-mail';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { showAgent, deriveLiveness } from './agent-registry';
|
||||
import { canonicalizeWindowsPath } from './pathing';
|
||||
import type { AgentMessage } from './agent-mail';
|
||||
|
||||
const MIN_TTL_MINUTES = 5;
|
||||
const MAX_TTL_MINUTES = 1440;
|
||||
|
|
@ -101,30 +102,13 @@ function messageIndexDirectoryPath(): string {
|
|||
return path.join(agentRoot(), 'messages', 'index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a path according to the Operative Protocol v1:
|
||||
* 1. Resolve to absolute path.
|
||||
* 2. Normalize separators to /.
|
||||
* 3. On Windows, lowercase normalized path.
|
||||
* 4. Remove trailing slash except root.
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
let resolved = path.resolve(p);
|
||||
// Normalize separators
|
||||
resolved = resolved.replace(/\\/g, '/');
|
||||
|
||||
// Lowercase on Windows
|
||||
if (process.platform === 'win32') {
|
||||
resolved = resolved.toLowerCase();
|
||||
}
|
||||
|
||||
// Remove trailing slash except root (e.g., C:/ or /)
|
||||
if (resolved.length > 3 && resolved.endsWith('/')) {
|
||||
resolved = resolved.slice(0, -1);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
/**
|
||||
* Normalizes a path using the canonicalization helpers from pathing module.
|
||||
* Converts to forward slashes for stable case-insensitive comparison.
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
return canonicalizeWindowsPath(p).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
export type OverlapClass = 'exact' | 'partial' | 'disjoint';
|
||||
|
||||
|
|
|
|||
|
|
@ -26,13 +26,16 @@ interface CacheEntry<T> {
|
|||
const agentCache = new Map<string, CacheEntry<AgentRecord | null>>();
|
||||
const CACHE_TTL_MS = 30_000;
|
||||
|
||||
function getCachedAgent(beadId: string): AgentRecord | null {
|
||||
function getCachedAgent(beadId: string): AgentRecord | null | undefined {
|
||||
const entry = agentCache.get(beadId);
|
||||
if (entry && entry.expiresAt > Date.now()) {
|
||||
return entry.data;
|
||||
if (!entry) {
|
||||
return undefined; // Cache miss
|
||||
}
|
||||
agentCache.delete(beadId);
|
||||
return null;
|
||||
if (entry.expiresAt > Date.now()) {
|
||||
return entry.data; // Valid cache hit (could be null or AgentRecord)
|
||||
}
|
||||
agentCache.delete(beadId); // Expired entry
|
||||
return null; // Treat expired as miss
|
||||
}
|
||||
|
||||
function setCachedAgent(beadId: string, data: AgentRecord | null): void {
|
||||
|
|
@ -82,7 +85,7 @@ function trimOrEmpty(value: unknown): string {
|
|||
async function callBdAgentShow(beadId: string, projectRoot: string): Promise<AgentRecord | null> {
|
||||
const cached = getCachedAgent(beadId);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
return cached; // Valid cache hit (could be null or AgentRecord)
|
||||
}
|
||||
|
||||
const showResult = await runBdCommand({
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import path from 'node:path';
|
||||
import { canonicalizeWindowsPath } from './pathing';
|
||||
|
||||
function isWindowsAbsolute(input: string): boolean {
|
||||
return /^[A-Za-z]:[\\/]/.test(input);
|
||||
}
|
||||
|
||||
function windowsToPosixMount(input: string): string {
|
||||
const drive = input[0].toLowerCase();
|
||||
const tail = input.slice(2).replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
return `/mnt/${drive}/${tail}`;
|
||||
const normalized = canonicalizeWindowsPath(input);
|
||||
const drive = normalized[0]?.toLowerCase() || '';
|
||||
const tail = normalized.slice(2)?.replace(/\\/g, '/')?.replace(/^\/+/, '') || '';
|
||||
if (drive && tail) {
|
||||
return `/mnt/${drive}/${tail}`;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function normalizeProjectRootForRuntime(input: string): string {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,30 @@ export function resolveIssuesJsonlPath(projectRoot: string = process.cwd()): str
|
|||
return resolveIssuesJsonlPathCandidates(projectRoot)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write issues to disk using BD audit record when available.
|
||||
* This ensures all writes go through the BD audit system for watcher/SSE parity.
|
||||
*/
|
||||
export async function writeIssuesToDisk(
|
||||
issues: BeadIssueWithProject[],
|
||||
options: ReadIssuesOptions = {}
|
||||
): Promise<void> {
|
||||
const projectRoot = options.projectRoot ?? process.cwd();
|
||||
const issuesJson = JSON.stringify(issues, null, 2);
|
||||
|
||||
try {
|
||||
const { execFileSync } = await import('child_process');
|
||||
execFileSync('bd', ['audit', 'record', '--stdin'], {
|
||||
input: issuesJson,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} catch {
|
||||
const issuesPath = resolveIssuesJsonlPath(projectRoot);
|
||||
const { writeFile } = await import('node:fs/promises');
|
||||
await writeFile(issuesPath, issuesJson, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readIssuesFromDisk(options: ReadIssuesOptions = {}): Promise<BeadIssueWithProject[]> {
|
||||
const projectRoot = options.projectRoot ?? process.cwd();
|
||||
const project = buildProjectContext(projectRoot, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue