Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/setup-workspace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ echo "Replacing path dependencies with crates.io versions..."

# core/Cargo.toml — internal crate deps
sed -i.bak \
-e 's|a3s-common = { version = "0.1.1", path = "../../common" }|a3s-common = "0.1.1"|' \
-e 's|a3s-common = { version = "0.1", path = "../../common" }|a3s-common = "0.1.1"|' \
-e 's|a3s-memory = { version = "0.1.1", path = "../../memory" }|a3s-memory = "0.1.1"|' \
-e 's|a3s-lane = { version = "0.4", path = "../../lane" }|a3s-lane = "0.4"|' \
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ default_model = "anthropic/claude-sonnet-4-20250514"

providers "anthropic" {
apiKey = env("ANTHROPIC_API_KEY")

models "claude-sonnet-4-20250514" {
limit = {
context = 200000
output = 8192
}
}
}
```

Expand Down Expand Up @@ -1267,6 +1274,13 @@ default_model = "anthropic/claude-sonnet-4-20250514"

providers "anthropic" {
apiKey = env("ANTHROPIC_API_KEY")

models "claude-sonnet-4-20250514" {
limit = {
context = 200000
output = 8192
}
}
}

skill_dirs = ["./skills"]
Expand All @@ -1279,6 +1293,10 @@ ahp = {
}
```

Model token limits use the `limit = { context = ..., output = ... }` object as the canonical ACL shape.
The flat `maxTokens` and `contextTokens` fields are accepted only as deprecated
migration aliases and emit warnings.

---

## Development
Expand Down
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ path = "src/lib.rs"

[dependencies]
# Internal crates
a3s-common = { version = "0.1", path = "../../common" }
a3s-common = { version = "0.1.1", path = "../../common" }
a3s-memory = { version = "0.1.1", path = "../../memory" }
a3s-lane = { version = "0.4", path = "../../lane" }
a3s-search = { version = "1.2.3", path = "../../search", default-features = false, features = ["lightpanda"] }
Expand Down
1 change: 1 addition & 0 deletions core/skills/code-review.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: code-review
description: Review code for best practices, bugs, and improvements
allowed-tools: "grep(*), glob(*), read(*)"
kind: instruction
tags:
- review
Expand Down
1 change: 1 addition & 0 deletions core/skills/code-search.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: code-search
description: Search codebase for patterns, functions, or types
allowed-tools: "grep(*), glob(*), read(*)"
kind: instruction
tags:
- search
Expand Down
1 change: 1 addition & 0 deletions core/skills/explain-code.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: explain-code
description: Explain how code works in clear, simple terms
allowed-tools: "grep(*), glob(*), read(*)"
kind: instruction
tags:
- explain
Expand Down
1 change: 1 addition & 0 deletions core/skills/find-bugs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: find-bugs
description: Identify potential bugs, vulnerabilities, and code smells
allowed-tools: "grep(*), glob(*), read(*)"
kind: instruction
tags:
- bugs
Expand Down
8 changes: 3 additions & 5 deletions core/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,15 +627,13 @@ impl SessionCommand for ToolCommand {
async fn execute(&self) -> Result<Value> {
// Check skill-based tool permissions
if let Some(registry) = &self.skill_registry {
let instruction_skills = registry.by_kind(crate::skills::SkillKind::Instruction);

// If there are instruction skills with tool restrictions, check permissions
let has_restrictions = instruction_skills.iter().any(|s| s.allowed_tools.is_some());
let restricting_skills = registry.global_tool_restricting_skills();

if has_restrictions {
if !restricting_skills.is_empty() {
let mut allowed = false;

for skill in &instruction_skills {
for skill in &restricting_skills {
if skill.is_tool_allowed(&self.tool_name) {
allowed = true;
break;
Expand Down
49 changes: 49 additions & 0 deletions core/src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
}
}

fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
match value {
a3s_acl::Value::Number(value) if *value >= 0.0 => {
Some((*value as usize).min(u32::MAX as usize) as u32)
}
_ => None,
}
}

fn acl_object_u32_attr(value: &a3s_acl::Value, key: &str) -> Option<u32> {
match value {
a3s_acl::Value::Object(pairs) => pairs
.iter()
.find_map(|(candidate, value)| (candidate == key).then(|| acl_u32(value)).flatten()),
_ => None,
}
}

fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
let value = acl_attr(block, keys)?;
match value {
Expand Down Expand Up @@ -276,6 +294,37 @@ impl CodeConfig {
model.release_date = Some(release_date);
}
}
"maxTokens" => {
tracing::warn!(
provider = %provider.name,
model = %model.id,
field = "maxTokens",
"Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
);
if let Some(output) = acl_u32(value) {
model.limit.output = output;
}
}
"contextTokens" => {
tracing::warn!(
provider = %provider.name,
model = %model.id,
field = "contextTokens",
"Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
);
if let Some(context) = acl_u32(value) {
model.limit.context = context;
}
}
"limit" => {
if let Some(output) = acl_object_u32_attr(value, "output") {
model.limit.output = output;
}
if let Some(context) = acl_object_u32_attr(value, "context")
{
model.limit.context = context;
}
}
_ => {}
}
}
Expand Down
36 changes: 36 additions & 0 deletions core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,42 @@ fn test_config_supports_acl_style_provider_labels() {
assert!(config.providers[0].models[0].tool_call);
}

#[test]
fn test_config_parses_acl_model_token_limits() {
let config = CodeConfig::from_acl(
r#"
default_model = "openai/glm-5.1"

providers "openai" {
api_key = "sk-test"
base_url = "http://127.0.0.1:18051/v1"

models "glm-5.1" {
name = "GLM 5.1"
limit = {
output = 4096
context = 32768
}
}

models "flat-alias" {
maxTokens = 8192
contextTokens = 65536
}
}
"#,
)
.unwrap();

let flat = &config.providers[0].models[0].limit;
assert_eq!(flat.output, 4096);
assert_eq!(flat.context, 32768);

let flat_alias = &config.providers[0].models[1].limit;
assert_eq!(flat_alias.output, 8192);
assert_eq!(flat_alias.context, 65536);
}

#[test]
fn test_config_builder() {
let config = CodeConfig::new()
Expand Down
32 changes: 28 additions & 4 deletions core/src/safety_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,12 @@ impl<'a> ToolSafetyGate<'a> {

fn check_skill_restrictions(&self, tool_name: &str) -> Option<ToolGateDecision> {
let registry = self.config.skill_registry.as_ref()?;
let instruction_skills = registry.by_kind(crate::skills::SkillKind::Instruction);
let has_restrictions = instruction_skills.iter().any(|s| s.allowed_tools.is_some());
if !has_restrictions {
let restricting_skills = registry.global_tool_restricting_skills();
if restricting_skills.is_empty() {
return None;
}

let allowed = instruction_skills
let allowed = restricting_skills
.iter()
.any(|skill| skill.is_tool_allowed(tool_name));
if allowed {
Expand Down Expand Up @@ -220,6 +219,31 @@ mod tests {
));
}

#[tokio::test]
async fn builtin_skill_permissions_do_not_restrict_default_session_tools() {
let config = AgentConfig {
skill_registry: Some(Arc::new(SkillRegistry::with_builtins())),
permission_checker: Some(Arc::new(StaticPermission(PermissionDecision::Allow))),
..Default::default()
};
let gate = ToolSafetyGate::new(&config);

let decision = gate
.decide(ToolGateInput {
tool_name: "write",
args: &json!({"file_path": "x"}),
pre_tool_block: None,
})
.await;

assert!(matches!(
decision,
ToolGateDecision::Execute {
reason: ToolGateApproval::PermissionAllow,
}
));
}

#[tokio::test]
async fn hook_block_denies_before_permission_allow() {
let config = AgentConfig {
Expand Down
5 changes: 3 additions & 2 deletions core/src/skills/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ mod tests {
let skill = code_search_skill();
assert_eq!(skill.name, "code-search");
assert!(skill.content.contains("Code Search"));
assert!(skill.allowed_tools.is_none());
assert!(skill.is_tool_allowed("write"));
assert!(skill.allowed_tools.is_some());
assert!(skill.is_tool_allowed("read"));
assert!(!skill.is_tool_allowed("write"));
}

#[test]
Expand Down
Loading
Loading