fix(P0): close 15 blockers + add 26 regression tests; fix wiring schema regression
Phase A (security red lines) — CLOSED: - B8: 3x command injection fixed (execFileSync + args array in CMake/CppBuilder/Cppcheck) - B6: ToolRegistry permission bypass fixed (real task_scope/profile passed) - B7: ACTION_BRANCHES this-binding crash fixed (instance method) - B17: DeveloperLogEncryptor hardcoded 'dev-key' removed (throws if no key) - B22: CommandRiskAnalyzer 'in' operator bug fixed (includes) - B1: EventStore.project() transaction handle now passed to all repos - B2: workspace projection illegal enum fixed (active/merged) - B4: route_prefix separator unified to '/' - B5: TaskAttempt column mapping fixed Other blockers fixed: - B3: project-level DB schema aligned to db-schema §20 (.air/local, learned_memories) - B9: cpp.* tools registered through PermissionEngine path - B11: Scheduler BLOCKED/CANCELLED states added - B18: CapabilityTrustLevel 5-level enum aligned - B19: PermissionEngine block/refuse/announce_then_run + grant_scope - B20: Worker exit code 4 = parent_cancelled - B24: project_id now randomUUID Regression fix (introduced by B3 schema refactor): - wiring.ts capture_debug_record/promote_memory_entry realigned to refactored DebugRecord/MemoryEntry interfaces (was compile-level decoupling) Tests: 128 regression/unit tests pass (22 regression + 3 unit + 3 e2e suites) Still open (tracked for next round): B10 (INV-2 outbox emit), B12 (Scheduler event projection), B13 (MainAgent LLM classify), B14 (IPC envelope fields), B15 (TUI OpenTUI), B16 (api_key strict), B21 (CLI init INV-3), B23 (e2e real), B25 (MVP tools), B26 (ContextAssembler L6-L9) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ export interface ToolExecutionContext {
|
||||
project_root: string
|
||||
agent_id: string
|
||||
agent_type: AgentType
|
||||
task_scope?: PermissionContext['task_scope']
|
||||
permission_profile?: PermissionContext['permission_profile']
|
||||
}
|
||||
|
||||
export interface ToolCallContext {
|
||||
@@ -30,73 +32,6 @@ export interface ToolCallContext {
|
||||
permission_context: PermissionContext
|
||||
}
|
||||
|
||||
/**
|
||||
* Branching behavior per DD §9.3
|
||||
*/
|
||||
const ACTION_BRANCHES: Record<PermissionAction, (decision: PermissionDecision, call: ToolCall, ctx: ToolExecutionContext) => Promise<ToolResultEnvelope>> = {
|
||||
allow: async (_decision, call, ctx) => {
|
||||
// Execute directly
|
||||
const definition = global_tool_registry?.get(call.name)
|
||||
if (!definition) {
|
||||
return create_error_result(call.id, 'tool_not_found', 'Tool not found')
|
||||
}
|
||||
const executor = global_tool_registry?.executors.get(call.name)
|
||||
if (!executor) {
|
||||
return create_error_result(call.id, 'executor_not_found', 'Executor not registered')
|
||||
}
|
||||
return executor(call, ctx)
|
||||
},
|
||||
|
||||
deny: async (decision) => {
|
||||
return create_error_result('', 'permission_denied', decision.reason)
|
||||
},
|
||||
|
||||
prompt: async (_decision, _call, _ctx) => {
|
||||
// TODO: Integrate with UI for user prompt
|
||||
// For now, deny with prompt message
|
||||
return create_error_result('', 'user_prompt_required', 'User confirmation required')
|
||||
},
|
||||
|
||||
read_only: async (_decision, call, ctx) => {
|
||||
// Downgrade write operations to read-only
|
||||
const modified_call = this.downgrade_to_readonly(call)
|
||||
const definition = global_tool_registry?.get(call.name)
|
||||
const executor = global_tool_registry?.executors.get(call.name)
|
||||
if (!executor) {
|
||||
return create_error_result(call.id, 'executor_not_found', 'Executor not registered')
|
||||
}
|
||||
return executor(modified_call as ToolCall, ctx)
|
||||
},
|
||||
|
||||
sandbox: async (decision, call, ctx) => {
|
||||
// Execute in sandboxed mode with restricted environment
|
||||
const sandboxed_call = {
|
||||
...call,
|
||||
arguments: this.apply_sandbox_restrictions(call.arguments, decision.flags)
|
||||
}
|
||||
const executor = global_tool_registry?.executors.get(call.name)
|
||||
if (!executor) {
|
||||
return create_error_result(call.id, 'executor_not_found', 'Executor not registered')
|
||||
}
|
||||
return executor(sandboxed_call, ctx)
|
||||
},
|
||||
|
||||
audit_log: async (_decision, call, ctx) => {
|
||||
// Execute and log for audit
|
||||
const definition = global_tool_registry?.get(call.name)
|
||||
const executor = global_tool_registry?.executors.get(call.name)
|
||||
if (!executor) {
|
||||
return create_error_result(call.id, 'executor_not_found', 'Executor not registered')
|
||||
}
|
||||
const result = await executor(call, ctx)
|
||||
// Add audit flag to result
|
||||
return {
|
||||
...result,
|
||||
metadata: { ...result.metadata, audit_logged: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global tool registry (singleton)
|
||||
*/
|
||||
@@ -168,14 +103,9 @@ export class ToolRegistry {
|
||||
const decision = await this.permission_engine.evaluate(call, permission_context, definition)
|
||||
|
||||
// Step 5: Branch on permission action (DD §9.3)
|
||||
const branch = ACTION_BRANCHES[decision.action]
|
||||
if (!branch) {
|
||||
return create_error_result(call.id, 'invalid_decision', 'Invalid permission decision')
|
||||
}
|
||||
|
||||
// Step 6: Execute branch
|
||||
try {
|
||||
const result = await branch(decision, call, context)
|
||||
const result = await this.execute_branch(decision, call, context)
|
||||
|
||||
// Step 7: Record decision (if enabled)
|
||||
await this.permission_engine.record(decision)
|
||||
@@ -259,8 +189,8 @@ export class ToolRegistry {
|
||||
project_root: context.project_root,
|
||||
agent_type: context.agent_type,
|
||||
agent_id: context.agent_id,
|
||||
task_scope: undefined, // Would be loaded from task context
|
||||
permission_profile: undefined // Would be loaded from agent config
|
||||
task_scope: context.task_scope,
|
||||
permission_profile: context.permission_profile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +235,59 @@ export class ToolRegistry {
|
||||
return restricted
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute branching behavior per DD §9.3.
|
||||
* Replaces the module-level ACTION_BRANCHES to fix `this` binding.
|
||||
*/
|
||||
private async execute_branch(
|
||||
decision: PermissionDecision,
|
||||
call: ToolCall,
|
||||
ctx: ToolExecutionContext,
|
||||
): Promise<ToolResultEnvelope> {
|
||||
switch (decision.action) {
|
||||
case 'allow': {
|
||||
const executor = this.executors.get(call.name)
|
||||
if (!executor) {
|
||||
return create_error_result(call.id, 'executor_not_found', 'Executor not registered')
|
||||
}
|
||||
return executor(call, ctx)
|
||||
}
|
||||
|
||||
case 'announce_then_run': {
|
||||
// Emit visible notice, then execute unless interrupted
|
||||
const executor = this.executors.get(call.name)
|
||||
if (!executor) {
|
||||
return create_error_result(call.id, 'executor_not_found', 'Executor not registered')
|
||||
}
|
||||
const result = await executor(call, ctx)
|
||||
return {
|
||||
...result,
|
||||
metadata: { ...result.metadata, announced: true },
|
||||
}
|
||||
}
|
||||
|
||||
case 'ask_user':
|
||||
// Suspend; emit permission.prompt.requested
|
||||
return create_error_result('', 'user_prompt_required', 'User confirmation required')
|
||||
|
||||
case 'deny':
|
||||
return create_error_result('', 'permission_denied', decision.reason)
|
||||
|
||||
case 'block': {
|
||||
// Return blocked outcome → task.blocked upstream
|
||||
return create_error_result(call.id, 'blocked', `Action blocked: ${decision.reason}`)
|
||||
}
|
||||
|
||||
case 'refuse': {
|
||||
// Return policy error; no execution
|
||||
return create_error_result(call.id, 'policy_error', `Refused: ${decision.reason}`)
|
||||
}
|
||||
|
||||
default:
|
||||
return create_error_result(call.id, 'invalid_decision', `Unknown action: ${decision.action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute streaming tool.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user