Security
Gate privileged operations with a permission policy, a human-in-the-loop confirmation flow, and a security provider
Security
Every side effect an agent can produce — writing files, running bash, pushing
git — flows through a permission policy. Start from an ask or deny
fallback, then list the patterns that should be allow-ed, deny-ed, or sent
to the ask path. To keep a human in the loop, add a confirmation policy:
ask decisions pause on a confirmation_required event so your application
(or a person) can approve or reject each call. Use this whenever an agent runs
against a real repository.
import { Agent } from '@a3s-lab/code';
const agent = await Agent.create('agent.acl');
const session = agent.session(process.cwd(), {
permissionPolicy: {
allow: ['read(*)', 'grep(*)', 'glob(*)', 'ls(*)', 'bash(git status:*)'],
deny: ['write(**/.env*)', 'bash(rm -rf*)'],
ask: ['write(*)', 'edit(*)', 'bash(git push:*)', 'bash(npm publish:*)', 'bash(*)'],
defaultDecision: 'ask',
},
// Turn the `ask` patterns into a human-in-the-loop confirmation flow.
confirmationPolicy: {
enabled: true,
defaultTimeoutMs: 120000,
timeoutAction: 'reject',
},
});
// Stream execution and resolve confirmations as they arrive.
const stream = await session.stream('Bump the version and push the release');
while (true) {
const next = await stream.next();
if (next.done || !next.value) break;
const event = next.value;
if (event.type === 'confirmation_required') {
// Look up the pending request for richer display.
const [pending] = await session.pendingConfirmations();
const toolId = pending?.toolId ?? event.toolId;
console.log(`[confirm] ${pending?.toolName ?? event.toolName}`);
console.log(JSON.stringify(pending?.args ?? {}, null, 2));
// In a real app, prompt the user here.
const approved = false; // deny risky operations by default
if (toolId) await session.confirmToolUse(toolId, approved, 'Reviewed by host');
}
}
session.close();from a3s_code import Agent, SessionOptions, PermissionPolicy, ConfirmationPolicy
def main() -> None:
agent = Agent.create("agent.acl")
opts = SessionOptions()
opts.permission_policy = PermissionPolicy(
allow=["read(*)", "grep(*)", "glob(*)", "ls(*)", "bash(git status:*)"],
deny=["write(**/.env*)", "bash(rm -rf*)"],
ask=["write(*)", "edit(*)", "bash(git push:*)", "bash(npm publish:*)", "bash(*)"],
default_decision="ask",
)
# Turn the `ask` patterns into a human-in-the-loop confirmation flow.
opts.confirmation_policy = ConfirmationPolicy(
enabled=True,
default_timeout_ms=120_000,
timeout_action="reject",
)
session = agent.session(".", opts)
# Stream execution and resolve confirmations as they arrive.
for event in session.stream("Bump the version and push the release"):
if event.event_type == "confirmation_required":
# Look up the pending request for richer display.
pending = session.pending_confirmations()
first = pending[0] if pending else {}
tool_id = first.get("tool_id") or event.tool_id
print(f"[confirm] {first.get('tool_name') or event.tool_name}")
# In a real app, prompt the user here.
approved = False # deny risky operations by default
if tool_id:
session.confirm_tool_use(tool_id, approved, "Reviewed by host")
session.close()
if __name__ == "__main__":
main()Add a security provider
A DefaultSecurityProvider enables input taint tracking and output sanitisation,
screening tool I/O independently of the permission policy. Pass one through
securityProvider (Node) / security_provider (Python); omit it to disable
security entirely.
import { Agent, DefaultSecurityProvider } from '@a3s-lab/code';
const agent = await Agent.create('agent.acl');
const session = agent.session(process.cwd(), {
securityProvider: new DefaultSecurityProvider(),
permissionPolicy: {
allow: ['bash(echo:*)'],
ask: ['bash(*)'],
defaultDecision: 'ask',
},
});
// Privileged host operations run through the provider + policy.
const out = await session.bash('echo "screened by the security provider"');
console.log(out);
session.close();from a3s_code import Agent, SessionOptions, PermissionPolicy, DefaultSecurityProvider
def main() -> None:
agent = Agent.create("agent.acl")
opts = SessionOptions()
opts.security_provider = DefaultSecurityProvider()
opts.permission_policy = PermissionPolicy(
allow=["bash(echo:*)"],
ask=["bash(*)"],
default_decision="ask",
)
session = agent.session(".", opts)
# Privileged host operations run through the provider + policy.
out = session.bash('echo "screened by the security provider"')
print(out)
session.close()
if __name__ == "__main__":
main()Notes
defaultDecisionis the fallback for any pattern not matched byallow/deny/ask(one ofallow,deny, orask). Preferaskfor real repositories and open up only what automation needs.- A
confirmationPolicywithenabled: trueis what turnsaskdecisions into a pausingconfirmation_requiredevent. Resolve each one withsession.confirmToolUse(toolId, approved, reason?); if no answer arrives withindefaultTimeoutMs,timeoutAction(reject) decides the outcome. - Keep release and publish actions (
bash(git push*),bash(npm publish*)) on theaskordenypath unless automation owns the final step. - Direct host calls such as
session.tool(),session.bash(), andsession.git()are privileged host operations initiated by your application code. Authorize them in the host before calling the SDK; the permission policy above gates model-selected tool calls insidesend,run, andstream.
A runnable confirmation loop ships at
crates/code/sdk/node/examples/streaming/hitl_confirmation_loop.ts and
crates/code/sdk/python/examples/hitl_confirmation_loop.py.