fix: resolve 17 P2 defects and 5 P3 defects from pre-launch audit
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Batch fix covering multiple modules:
- P2-01: HandRegistry Semaphore-based max_concurrent enforcement
- P2-03: Populate toolCount/metricCount from Hand trait methods
- P2-06: heartbeat_update_config minimum interval validation
- P2-07: ReflectionResult used_fallback marker for rule-based fallback
- P2-08/09: identity_propose_change parameter naming consistency
- P2-10: ClassroomMetadata is_placeholder flag for LLM failure
- P2-11: classroomStore userDidCloseDuringGeneration intent tracking
- P2-12: workflowStore pipeline_create sends actionType
- P2-13/14: PipelineInfo step_count + PipelineStepInfo for proper step mapping
- P2-15: Pipe transform support in context.resolve (8 transforms)
- P2-16: Mustache {{...}} → \${...} auto-normalization
- P2-17: SaaSLogin password placeholder 6→8
- P2-19: serialize_skill_md + update_skill preserve tools field
- P2-22: ToolOutputGuard sensitive patterns from warn→block
- P2-23: Mutex::unwrap() → unwrap_or_else in relay/service.rs
- P3-01/03/07/08/09: Various P3 fixes
- DEFECT_LIST.md: comprehensive status sync (43/51 fixed, 8 remaining)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-06 00:49:16 +08:00
parent f9e1ce1d6e
commit 26a833d1c8
25 changed files with 408 additions and 143 deletions

View File

@@ -176,4 +176,14 @@ pub trait Hand: Send + Sync {
fn status(&self) -> HandStatus {
HandStatus::Idle
}
/// P2-03: Get the number of tools this hand exposes (default: 0)
fn tool_count(&self) -> u32 {
0
}
/// P2-03: Get the number of metrics this hand tracks (default: 0)
fn metric_count(&self) -> u32 {
0
}
}

View File

@@ -885,6 +885,16 @@ impl Hand for QuizHand {
}
async fn execute(&self, _context: &HandContext, input: Value) -> Result<HandResult> {
// P2-04: Reject oversized input before deserialization
const MAX_INPUT_SIZE: usize = 50_000; // 50KB limit
let input_str = serde_json::to_string(&input).unwrap_or_default();
if input_str.len() > MAX_INPUT_SIZE {
return Ok(HandResult::error(format!(
"Input too large ({} bytes, max {} bytes)",
input_str.len(), MAX_INPUT_SIZE
)));
}
let action: QuizAction = match serde_json::from_value(input) {
Ok(a) => a,
Err(e) => {

View File

@@ -2,15 +2,17 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{RwLock, Semaphore};
use zclaw_types::Result;
use super::{Hand, HandConfig, HandContext, HandResult, Trigger, TriggerConfig};
use super::{Hand, HandConfig, HandContext, HandResult};
/// Hand registry
/// Hand registry with per-hand concurrency control (P2-01)
pub struct HandRegistry {
hands: RwLock<HashMap<String, Arc<dyn Hand>>>,
configs: RwLock<HashMap<String, HandConfig>>,
/// Per-hand semaphores for max_concurrent enforcement (key: hand id)
semaphores: RwLock<HashMap<String, Arc<Semaphore>>>,
}
impl HandRegistry {
@@ -18,6 +20,7 @@ impl HandRegistry {
Self {
hands: RwLock::new(HashMap::new()),
configs: RwLock::new(HashMap::new()),
semaphores: RwLock::new(HashMap::new()),
}
}
@@ -27,6 +30,15 @@ impl HandRegistry {
let mut hands = self.hands.write().await;
let mut configs = self.configs.write().await;
// P2-01: Create semaphore for max_concurrent enforcement
if config.max_concurrent > 0 {
let mut semaphores = self.semaphores.write().await;
semaphores.insert(
config.id.clone(),
Arc::new(Semaphore::new(config.max_concurrent as usize)),
);
}
hands.insert(config.id.clone(), hand);
configs.insert(config.id.clone(), config);
}
@@ -49,7 +61,7 @@ impl HandRegistry {
configs.values().cloned().collect()
}
/// Execute a hand
/// Execute a hand with concurrency limiting (P2-01)
pub async fn execute(
&self,
id: &str,
@@ -59,73 +71,41 @@ impl HandRegistry {
let hand = self.get(id).await
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Hand not found: {}", id)))?;
hand.execute(context, input).await
// P2-01: Acquire semaphore permit if max_concurrent is set
let semaphore_opt = {
let semaphores = self.semaphores.read().await;
semaphores.get(id).cloned()
};
if let Some(semaphore) = semaphore_opt {
let _permit = semaphore.acquire().await
.map_err(|_| zclaw_types::ZclawError::Internal(
format!("Hand '{}' semaphore closed", id)
))?;
hand.execute(context, input).await
} else {
hand.execute(context, input).await
}
}
/// Remove a hand
pub async fn remove(&self, id: &str) {
let mut hands = self.hands.write().await;
let mut configs = self.configs.write().await;
let mut semaphores = self.semaphores.write().await;
hands.remove(id);
configs.remove(id);
semaphores.remove(id);
}
}
impl Default for HandRegistry {
fn default() -> Self {
Self::new()
}
}
/// Trigger registry
pub struct TriggerRegistry {
triggers: RwLock<HashMap<String, Arc<dyn Trigger>>>,
configs: RwLock<HashMap<String, TriggerConfig>>,
}
impl TriggerRegistry {
pub fn new() -> Self {
Self {
triggers: RwLock::new(HashMap::new()),
configs: RwLock::new(HashMap::new()),
/// P2-03: Get tool and metric counts for a hand
pub async fn get_counts(&self, id: &str) -> (u32, u32) {
let hands = self.hands.read().await;
if let Some(hand) = hands.get(id) {
(hand.tool_count(), hand.metric_count())
} else {
(0, 0)
}
}
/// Register a trigger
pub async fn register(&self, trigger: Arc<dyn Trigger>) {
let config = trigger.config().clone();
let mut triggers = self.triggers.write().await;
let mut configs = self.configs.write().await;
triggers.insert(config.id.clone(), trigger);
configs.insert(config.id.clone(), config);
}
/// Get a trigger by ID
pub async fn get(&self, id: &str) -> Option<Arc<dyn Trigger>> {
let triggers = self.triggers.read().await;
triggers.get(id).cloned()
}
/// List all triggers
pub async fn list(&self) -> Vec<TriggerConfig> {
let configs = self.configs.read().await;
configs.values().cloned().collect()
}
/// Remove a trigger
pub async fn remove(&self, id: &str) {
let mut triggers = self.triggers.write().await;
let mut configs = self.configs.write().await;
triggers.remove(id);
configs.remove(id);
}
}
impl Default for TriggerRegistry {
fn default() -> Self {
Self::new()
}
}