Skip to content

Commit 97d34f8

Browse files
hikejsclaude
andcommitted
chore(release): bump version to 1.0.2
- Add CHANGELOG entries for v1.0.1 and v1.0.2 - Align index.d.ts with Rust Node.js binding (13 missing methods) - Bump all package versions to 1.0.2 - Convert Node.js examples from .js to .ts - Update Python examples to sync API - Add memory tier APIs, queue metrics, security hardening Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent decd436 commit 97d34f8

64 files changed

Lines changed: 8654 additions & 6050 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-cli"
3-
version = "1.0.0"
3+
version = "1.0.2"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,40 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
---
99

10+
## [1.0.2] - 2026-03-04
11+
12+
### New Features
13+
14+
- **HCL `env()` function**`env("VAR")` resolves environment variables at parse time; returns null if unset.
15+
- **HCL verbatim blocks**`env {}` and `headers {}` blocks preserve keys verbatim (no camelCase conversion).
16+
- **Memory tier APIs**`getWorking()`, `clearWorking()`, `getShortTerm()`, `clearShortTerm()` exposed in Node.js and Python SDKs.
17+
- **Queue metrics**`queueMetrics()` returns a `MetricsSnapshot` with counters, gauges, and histograms.
18+
19+
### Improvements
20+
21+
- **Security hardening** — Enhanced default redaction patterns and robust regex in grep/ls tools.
22+
- **Windows path compatibility**`canonicalize()` now strips `\\?\` UNC prefix on Windows.
23+
- **RwLock poison recovery**`read_or_recover()` / `write_or_recover()` prevent panics from poisoned locks.
24+
- **Node.js TypeScript definitions** — Added 13 missing method declarations to `index.d.ts` (lane queue, memory API, session metadata).
25+
- **Documentation** — All example pages updated: Python sync API, TypeScript code blocks, correct event field names.
26+
27+
---
28+
29+
## [1.0.1] - 2026-03-03
30+
31+
### New Features
32+
33+
- **MCP live tool injection** — Agent loop refreshes tool list from live `ToolExecutor` each turn.
34+
- **MCP error tracking**`McpManager.connect_errors` tracks per-server failures, exposed in `mcpStatus()`.
35+
- **SDK introspection**`toolNames()`, `toolDefinitions()`, `refreshMcpTools()` in both Node.js and Python SDKs.
36+
- **MCP removal**`removeMcpServer()` for session-level MCP cleanup.
37+
38+
### Bug Fixes
39+
40+
- **Node.js stream fix**`stream()` return type corrected to `Promise<EventStream>` in TypeScript definitions.
41+
42+
---
43+
1044
## [1.0.0] - 2026-03-02
1145

1246
### New Features

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-core"
3-
version = "1.0.1"
3+
version = "1.0.2"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/src/agent_api.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -819,9 +819,10 @@ impl Agent {
819819

820820
// Register task delegation tools (task, parallel_task).
821821
// These require an LLM client to spawn isolated child agent loops.
822+
// When MCP manager is available, pass it through so child sessions inherit MCP tools.
822823
{
823824
use crate::subagent::{load_agents_from_dir, AgentRegistry};
824-
use crate::tools::register_task;
825+
use crate::tools::register_task_with_mcp;
825826
let agent_registry = AgentRegistry::new();
826827
for dir in self
827828
.code_config
@@ -833,11 +834,12 @@ impl Agent {
833834
agent_registry.register(agent);
834835
}
835836
}
836-
register_task(
837+
register_task_with_mcp(
837838
tool_executor.registry(),
838839
Arc::clone(&llm_client),
839840
Arc::new(agent_registry),
840841
canonical.display().to_string(),
842+
opts.mcp_manager.clone(),
841843
);
842844
}
843845

core/src/permissions.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@ pub enum PermissionDecision {
4242
/// - `Read(src/**/*.rs)` - matches Rust files in src/
4343
/// - `Grep(*)` - matches all grep invocations
4444
/// - `mcp__pencil` - matches all pencil MCP tools
45-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45+
///
46+
/// Deserialization supports both plain strings and `{rule: "..."}` objects:
47+
/// ```yaml
48+
/// allow:
49+
/// - read # plain string
50+
/// - rule: "Bash(cargo:*)" # struct form
51+
/// ```
52+
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
4653
pub struct PermissionRule {
4754
/// The original rule string
4855
pub rule: String,
@@ -54,6 +61,28 @@ pub struct PermissionRule {
5461
arg_pattern: Option<String>,
5562
}
5663

64+
impl<'de> Deserialize<'de> for PermissionRule {
65+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66+
where
67+
D: serde::Deserializer<'de>,
68+
{
69+
/// Helper enum to accept both `"read"` and `{rule: "read"}` in YAML/JSON.
70+
#[derive(Deserialize)]
71+
#[serde(untagged)]
72+
enum RuleRepr {
73+
Plain(String),
74+
Struct { rule: String },
75+
}
76+
77+
let rule_str = match RuleRepr::deserialize(deserializer)? {
78+
RuleRepr::Plain(s) => s,
79+
RuleRepr::Struct { rule } => rule,
80+
};
81+
// `new()` calls `parse_rule()` to populate tool_name and arg_pattern.
82+
Ok(PermissionRule::new(&rule_str))
83+
}
84+
}
85+
5786
impl PermissionRule {
5887
/// Create a new permission rule from a pattern string
5988
pub fn new(rule: &str) -> Self {
@@ -827,6 +856,55 @@ mod tests {
827856
assert!(policy.is_allowed("Grep", &json!({"pattern": "foo"})));
828857
}
829858

859+
// ========================================================================
860+
// PermissionRule Deserialization Tests
861+
// ========================================================================
862+
863+
#[test]
864+
fn test_rule_deserialize_plain_string() {
865+
// YAML: `- read`
866+
let rule: PermissionRule = serde_yaml::from_str("read").unwrap();
867+
assert_eq!(rule.rule, "read");
868+
assert!(rule.matches("read", &json!({})));
869+
assert!(!rule.matches("write", &json!({})));
870+
}
871+
872+
#[test]
873+
fn test_rule_deserialize_plain_string_with_pattern() {
874+
// YAML: `- "Bash(cargo:*)"`
875+
let rule: PermissionRule = serde_yaml::from_str("\"Bash(cargo:*)\"").unwrap();
876+
assert_eq!(rule.rule, "Bash(cargo:*)");
877+
assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
878+
}
879+
880+
#[test]
881+
fn test_rule_deserialize_struct_form() {
882+
// YAML: `- rule: read`
883+
let rule: PermissionRule = serde_yaml::from_str("rule: read").unwrap();
884+
assert_eq!(rule.rule, "read");
885+
assert!(rule.matches("read", &json!({})));
886+
}
887+
888+
#[test]
889+
fn test_rule_deserialize_in_policy() {
890+
// Full policy YAML with mixed formats
891+
let yaml = r#"
892+
allow:
893+
- read
894+
- "Bash(cargo:*)"
895+
- rule: grep
896+
deny:
897+
- write
898+
"#;
899+
let policy: PermissionPolicy = serde_yaml::from_str(yaml).unwrap();
900+
assert_eq!(policy.allow.len(), 3);
901+
assert_eq!(policy.deny.len(), 1);
902+
assert!(policy.is_allowed("read", &json!({})));
903+
assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
904+
assert!(policy.is_allowed("grep", &json!({})));
905+
assert!(policy.is_denied("write", &json!({})));
906+
}
907+
830908
// ========================================================================
831909
// PermissionManager Tests
832910
// ========================================================================

core/src/subagent.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,35 @@ permissions:
643643
assert_eq!(agent.name, "restricted-agent");
644644
assert_eq!(agent.permissions.allow.len(), 2);
645645
assert_eq!(agent.permissions.deny.len(), 1);
646+
// Verify that deserialized rules actually match (tool_name populated)
647+
assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
648+
assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
649+
assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
650+
}
651+
652+
#[test]
653+
fn test_parse_agent_yaml_with_plain_string_permissions() {
654+
// Users naturally write plain strings in allow/deny lists
655+
let yaml = r#"
656+
name: plain-agent
657+
description: Agent with plain string permissions
658+
permissions:
659+
allow:
660+
- read
661+
- grep
662+
- "Bash(cargo:*)"
663+
deny:
664+
- write
665+
"#;
666+
let agent = parse_agent_yaml(yaml).unwrap();
667+
assert_eq!(agent.name, "plain-agent");
668+
assert_eq!(agent.permissions.allow.len(), 3);
669+
assert_eq!(agent.permissions.deny.len(), 1);
670+
// Verify rules are functional
671+
assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
672+
assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
673+
assert!(agent.permissions.allow[2].matches("Bash", &serde_json::json!({"command": "cargo build"})));
674+
assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
646675
}
647676

648677
#[test]

core/src/tools/builtin/mod.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,32 @@ pub fn register_batch(registry: &Arc<ToolRegistry>) {
5757
///
5858
/// Must be called after the registry is wrapped in Arc. Requires an LLM client
5959
/// and the workspace path so child agent loops can be spawned inline.
60+
/// Optionally accepts an MCP manager so child sessions inherit MCP tools.
6061
pub fn register_task(
6162
registry: &Arc<ToolRegistry>,
6263
llm_client: Arc<dyn crate::llm::LlmClient>,
6364
agent_registry: Arc<crate::subagent::AgentRegistry>,
6465
workspace: String,
66+
) {
67+
register_task_with_mcp(registry, llm_client, agent_registry, workspace, None);
68+
}
69+
70+
/// Register the task delegation tools with optional MCP manager.
71+
///
72+
/// When `mcp_manager` is provided, child subagent sessions will have access
73+
/// to all MCP tools from connected servers.
74+
pub fn register_task_with_mcp(
75+
registry: &Arc<ToolRegistry>,
76+
llm_client: Arc<dyn crate::llm::LlmClient>,
77+
agent_registry: Arc<crate::subagent::AgentRegistry>,
78+
workspace: String,
79+
mcp_manager: Option<Arc<crate::mcp::manager::McpManager>>,
6580
) {
6681
use crate::tools::task::{ParallelTaskTool, TaskExecutor, TaskTool};
67-
let executor = Arc::new(TaskExecutor::new(agent_registry, llm_client, workspace));
82+
let executor = Arc::new(match mcp_manager {
83+
Some(mcp) => TaskExecutor::with_mcp(agent_registry, llm_client, workspace, mcp),
84+
None => TaskExecutor::new(agent_registry, llm_client, workspace),
85+
});
6886
registry.register_builtin(Arc::new(TaskTool::new(Arc::clone(&executor))));
6987
registry.register_builtin(Arc::new(ParallelTaskTool::new(executor)));
7088
}

core/src/tools/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ mod registry;
1515
pub mod task;
1616
mod types;
1717

18-
pub use builtin::register_task;
18+
pub use builtin::{register_task, register_task_with_mcp};
1919
pub use registry::ToolRegistry;
2020
pub use task::{
2121
parallel_task_params_schema, task_params_schema, ParallelTaskParams, ParallelTaskTool,

core/src/tools/task.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
1717
use crate::agent::{AgentConfig, AgentEvent, AgentLoop};
1818
use crate::llm::LlmClient;
19+
use crate::mcp::manager::McpManager;
1920
use crate::subagent::AgentRegistry;
2021
use crate::tools::types::{Tool, ToolContext, ToolOutput};
2122
use anyhow::{Context, Result};
@@ -66,6 +67,8 @@ pub struct TaskExecutor {
6667
llm_client: Arc<dyn LlmClient>,
6768
/// Workspace path shared with child agents
6869
workspace: String,
70+
/// Optional MCP manager for registering MCP tools in child sessions
71+
mcp_manager: Option<Arc<McpManager>>,
6972
}
7073

7174
impl TaskExecutor {
@@ -79,6 +82,22 @@ impl TaskExecutor {
7982
registry,
8083
llm_client,
8184
workspace,
85+
mcp_manager: None,
86+
}
87+
}
88+
89+
/// Create a new task executor with MCP manager for tool inheritance
90+
pub fn with_mcp(
91+
registry: Arc<AgentRegistry>,
92+
llm_client: Arc<dyn LlmClient>,
93+
workspace: String,
94+
mcp_manager: Arc<McpManager>,
95+
) -> Self {
96+
Self {
97+
registry,
98+
llm_client,
99+
workspace,
100+
mcp_manager: Some(mcp_manager),
82101
}
83102
}
84103

@@ -109,6 +128,24 @@ impl TaskExecutor {
109128
// Build a child ToolExecutor. Task tools are intentionally omitted
110129
// here to prevent unlimited subagent nesting.
111130
let mut child_executor = crate::tools::ToolExecutor::new(self.workspace.clone());
131+
132+
// Register MCP tools so child agents can access MCP servers.
133+
if let Some(ref mcp) = self.mcp_manager {
134+
let all_tools = mcp.get_all_tools().await;
135+
let mut by_server: std::collections::HashMap<String, Vec<crate::mcp::protocol::McpTool>> =
136+
std::collections::HashMap::new();
137+
for (server, tool) in all_tools {
138+
by_server.entry(server).or_default().push(tool);
139+
}
140+
for (server_name, tools) in by_server {
141+
let wrappers =
142+
crate::mcp::tools::create_mcp_tools(&server_name, tools, Arc::clone(mcp));
143+
for wrapper in wrappers {
144+
child_executor.register_dynamic_tool(wrapper);
145+
}
146+
}
147+
}
148+
112149
if !agent.permissions.allow.is_empty() || !agent.permissions.deny.is_empty() {
113150
child_executor.set_guard_policy(Arc::new(agent.permissions.clone())
114151
as Arc<dyn crate::permissions::PermissionChecker>);

0 commit comments

Comments
 (0)