A3S Docs
A3S Code

Security

Permission system, HITL confirmation, and extensible security traits

Security

A3S Code provides two built-in security layers: Permission Policy and Human-in-the-Loop (HITL) Confirmation. Additional security logic can be implemented via the SecurityProvider trait and HookEngine.

Security Architecture

User Prompt → Agent Loop
  ├─ Permission Policy    ← Deny → Allow → Ask → Default
  ├─ HITL Confirmation    ← Independent of permissions
  ├─ Tool Execution
  └─ SecurityProvider     ← Pluggable via trait (taint tracking, sanitization, etc.)

Permission Policy

The permission system controls which tools the agent can use. Rules are evaluated in order: Deny → Allow → Ask → Default.

Rule Types

Prop

Type

Pattern Matching

Rules use glob-style pattern matching on tool names and arguments:

Tool(pattern)

Prop

Type

Wildcards: * matches any character except /, ** matches including /, :* matches any suffix.

Preset Policies

Prop

Type

Building a Custom Policy

use a3s_code_core::permissions::PermissionPolicy;

let policy = PermissionPolicy::new()
    .deny("bash(rm -rf:*)")
    .deny("bash(curl:*|sh)")
    .allow("read(*)")
    .allow("glob(*)")
    .allow("grep(*)")
    .ask("write(*)")
    .ask("bash(*)");

// Apply to session
session.set_permission_policy(policy).await;
const { PermissionPolicy } = require('@a3s-lab/code');

const policy = new PermissionPolicy()
  .deny('bash(rm -rf:*)')
  .deny('bash(curl:*|sh)')
  .allow('read(*)')
  .allow('glob(*)')
  .allow('grep(*)')
  .ask('write(*)')
  .ask('bash(*)');

await session.setPermissionPolicy(policy);
from a3s_code import PermissionPolicy

policy = PermissionPolicy()
policy.deny("bash(rm -rf:*)")
policy.deny("bash(curl:*|sh)")
policy.allow("read(*)")
policy.allow("glob(*)")
policy.allow("grep(*)")
policy.ask("write(*)")
policy.ask("bash(*)")

session.set_permission_policy(policy)

Evaluation Order

Rules are evaluated in this order:

Deny rules — If any deny rule matches, block immediately
Allow rules — If any allow rule matches, permit
Ask rules — If any ask rule matches, trigger HITL confirmation
Default policy — If no rules match, use the default (permissive or strict)

Example:

let policy = PermissionPolicy::new()
    .deny("bash(rm -rf:*)")      // Block destructive commands
    .allow("bash(git *)")         // Allow git commands
    .ask("bash(*)")               // Ask for all other bash commands
    .allow("read(*)")             // Allow all reads
    .ask("write(*)");             // Ask for all writes

Human-in-the-Loop (HITL)

HITL confirmation is independent of the permission system. Even if a tool is allowed by permissions, HITL can still require user confirmation.

Configuration

use a3s_code_core::hitl::{ConfirmationPolicy, TimeoutAction};

let hitl = ConfirmationPolicy {
    enabled: true,
    default_timeout_ms: 30_000,
    timeout_action: TimeoutAction::Reject,
    ..Default::default()
};

let session = agent.session("/project", Some(
    SessionOptions::new().with_confirmation_policy(hitl)
))?;
const session = agent.session('/project', {
  confirmationPolicy: {
    enabled: true,
    timeoutMs: 30000,
    timeoutAction: 'reject',
  },
});
from a3s_code import ConfirmationPolicy

session = agent.session("/project",
    confirmation_policy=ConfirmationPolicy(
        enabled=True,
        timeout_ms=30000,
        timeout_action="reject",
    ))

Handling Confirmation Events

let (mut rx, _handle) = session.stream("Clean up old logs").await?;
while let Some(event) = rx.recv().await {
    match event {
        AgentEvent::ConfirmationRequired { tool_id, tool_name, args, .. } => {
            println!("🔒 Approve '{tool_name}'? {args:?}");
            // Approve or reject
            session.respond_confirmation(&tool_id, true, None).await;
        }
        AgentEvent::PermissionDenied { tool_name, reason, .. } => {
            println!("🚫 {tool_name} blocked: {reason}");
        }
        AgentEvent::TextDelta { text } => print!("{text}"),
        AgentEvent::End { .. } => break,
        _ => {}
    }
}
for await (const event of session.stream('Clean up old logs')) {
  if (event.type === 'confirmation_required') {
    console.log(`🔒 Approve '${event.toolName}'?`, event.toolArgs);
    await session.respondConfirmation(event.toolId, { approved: true });
  } else if (event.type === 'permission_denied') {
    console.log(`🚫 ${event.toolName} blocked: ${event.reason}`);
  } else if (event.type === 'text_delta') {
    process.stdout.write(event.text);
  }
}
for event in session.stream("Clean up old logs"):
    if event.event_type == "confirmation_required":
        print(f"🔒 Approve '{event.tool_name}'? {event.tool_args}")
        session.respond_confirmation(event.tool_id, approved=True)
    elif event.event_type == "permission_denied":
        print(f"🚫 {event.tool_name} blocked: {event.reason}")
    elif event.event_type == "text_delta":
        print(event.text, end="", flush=True)

Timeout Behavior

Prop

Type

Workspace Boundaries

All file operations are restricted to the session's workspace directory. Attempts to access files outside the workspace are blocked.

let session = agent.session("/my-project", None)?;

// ✅ Allowed: within workspace
session.read_file("src/main.rs").await?;

// ❌ Blocked: outside workspace
session.read_file("../other-project/secret.txt").await?;

Extensible Security via Traits

A3S Code provides a SecurityProvider trait for implementing custom security logic:

pub trait SecurityProvider: Send + Sync {
    /// Classify and register sensitive data found in input text
    fn taint_input(&self, text: &str) {}

    /// Sanitize output text by redacting sensitive data
    fn sanitize_output(&self, text: &str) -> String {
        text.to_string()
    }

    /// Securely wipe all session security state
    fn wipe(&self) {}

    /// Register security hooks with the given engine
    fn register_hooks(&self, hook_engine: &HookEngine) {}

    /// Unregister all hooks from the engine
    fn teardown(&self, hook_engine: &HookEngine) {}
}

Example: Custom Security Provider

use a3s_code_core::security::SecurityProvider;
use a3s_code_core::hooks::{HookEngine, HookEvent, HookResult};

struct MySecurityProvider {
    // Your security state
}

impl SecurityProvider for MySecurityProvider {
    fn taint_input(&self, text: &str) {
        // Detect and track sensitive data (SSN, API keys, etc.)
    }

    fn sanitize_output(&self, text: &str) -> String {
        // Redact sensitive data from LLM output
        text.replace("sk-ant-", "[REDACTED]")
    }

    fn register_hooks(&self, hook_engine: &HookEngine) {
        // Register pre/post tool use hooks for security checks
        hook_engine.register(/* your hooks */);
    }
}

// Use in session
let security = Arc::new(MySecurityProvider { /* ... */ });
let session = agent.session("/project", Some(
    SessionOptions::new().with_security_provider(security)
))?;

See Hooks for lifecycle event integration.

Best Practices

Start with strict policy — Use PermissionPolicy::strict() and explicitly allow only needed tools
Block dangerous patterns — Always deny destructive bash commands: bash(rm -rf:*), bash(curl:*|sh)
Enable HITL for writes — Require confirmation for file modifications and bash execution
Use workspace boundaries — Never disable workspace restrictions
Implement SecurityProvider — Add custom taint tracking and output sanitization for sensitive data
Audit logs — Use hooks to log all tool executions for security review

Security Events

The agent emits security-related events during streaming:

Prop

Type

See Sessions for full event reference.

API Reference

PermissionPolicy

Prop

Type

Pattern syntax

Prop

Type

ConfirmationPolicy

Prop

Type

SecurityProvider trait (Rust)

Prop

Type

On this page