DEV Community

Junxiang Pan
Junxiang Pan

Posted on Edited on AI-assisted

Building a Windows-native ACL sandbox for self-hosted LLM agents (and what still fails)

Disclosure: I used AI to help polish the English in this post. The architecture, code, and testing are my own.

Why I stopped trying to “detect” bad AI behavior

I run a self-hosted LLM agent on Windows (AstrBot + a QQ adapter). Like many people in this space, I started by trying to block dangerous commands at the application layer — regex patterns for rm, del, shell keywords, etc.

It didn’t work. A plugin can execute arbitrary code. A prompt injection can rephrase a delete command in a dozen ways. A community member pointed out the obvious: you can’t statically predict what a Turing-complete program will do. Static scanning is not a sandbox.

So I changed the question. Instead of asking “is this action dangerous?”, I asked: “what is this process physically allowed to touch?”

What CBS actually does

Cyber-Butler-Security (CBS) is a Windows-first, OS-level security layer. It does not try to understand the agent’s intent. It restricts the agent’s physical permissions.

  • Dual-account isolation: AstrBot runs under a dedicated low-privilege Windows service account. Your desktop files are simply not visible to it via ACLs. It cannot read what it cannot see.
  • TOTP Broker for high-risk actions: Deletion or overwrite in a shared workspace is intercepted by a SYSTEM-level Broker. It requires a TOTP code from your phone before proceeding. This is not an AI judging the AI — it’s deterministic code enforcing a rule.
  • Fail-closed: If the security layer fails, it denies. The worst-case bug is a tool error, not a deleted C: drive.

No extra LLM tokens are consumed. The security decisions are made by OS-level code, not by the model.

What still fails (honest gaps)

I’m not going to pretend this is a complete security product. It’s early Beta.

  • The High security tier is still undergoing end-to-end verification on clean Windows VMs. Medium works; High has known service-startup issues I’m actively debugging.
  • There is no runtime sandbox (no Job Objects / process behavior monitoring yet). Static ACLs limit what the process can touch, but not what it does at runtime.
  • Python/Shell execution can bypass TOTP if those tools are enabled in the agent. The README documents this explicitly.
  • Plugin permission contracts don’t exist yet. “Installed” ≠ “runs correctly under ACL restrictions.”
  • Zero third-party audit. I’m an independent developer, and this is a side project.

Why not just use Docker?

Docker/WSL2 is the gold standard for isolation. But a large portion of the “养鱼” community runs on Windows bare metal — gaming PCs, laptops without virtualization enabled, users who won’t set up containers just to run an agent.

CBS is a pragmatic fallback for that group. It’s not a replacement for containers. It’s a lower-effort “safety net” for people who won’t (or can’t) containerize.

What I’m looking for

If you’ve run self-hosted agents on Windows, I’d like to hear:

  1. Is the TOTP approval flow practical for daily use, or too heavy?
  2. Are there Windows-native isolation primitives (Job Objects, AppContainer, Restricted Tokens) I should be using instead of ACLs alone?
  3. For plugin ecosystems: is a permission-declaration metadata field worth the effort, or is runtime fail-closed enough?

Repo (AGPL-3.0, not monetized, early Beta): https://github.com/JunxiangPan/Cyber-Butler-Security

README has the “not production-ready / High tier unverified” banners at the top. Feedback is welcome, including “this won’t work because X.”

UPDATE (Current Status):

After great feedback from the r/AI_Agents community, the architecture is now working end-to-end.

Scoped Tokens: Users can approve a 30-minute directory-bound session. This solves the "TOTP spam" issue for batch operations. The agent works freely inside the scoped folder but is blocked the moment it tries to step outside.

Job Object Enforcement: The Broker now uses JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. If the security layer crashes, all orphaned child processes are terminated by the Windows kernel.

A Vindication: DeepSeek's new "Super-agent" explicitly recommends "running under a separate OS account for isolation." This confirms the core premise of CBS. Autonomous agents need physical OS boundaries, not just application-layer prompts. CBS provides the automated installer, ACLs, and TOTP fallback for this exact use case.

Still in Beta, but the "Fail-Closed" loop is fully functional now.

Top comments (8)

Collapse
 
autenai profile image
Auten •

On your question 2: does the agent ever need to see or click the user's desktop, or is it shell and plugins only? If it is service-account only, session 0 isolation already keeps it off the interactive desktop, which is a nice side effect for free.

One tip if you move to AppContainer or a low-integrity restricted token: UIPI will also stop it from sending input to normal windows, and the input is just dropped with no clear error. Worth a fail-closed log line there, or users will think a GUI plugin is broken. (I'm on the Auten team, we build screen control for agents, so we run into this one a lot.)

Collapse
 
_9439daaf10225d6b438d6 profile image
Junxiang Pan •

This is a goldmine of a comment. The Session 0 isolation point is spot on — the service account gives us that for free, which is why I started there.

The UIPI silent-drop behavior is a nightmare I want to avoid. I am definitely adding a fail-closed log line for that specific case when I move to AppContainer or restricted tokens. If the agent is trying to interact with a GUI and the input is just dropped, users will 100% blame the plugin. Thanks for the warning!

On your question: the agent currently only needs shell/plugins, not interactive desktop access. So Session 0 works perfectly for now.

Collapse
 
autenai profile image
Auten •

Makes sense, Session 0 is a clean default for shell-only. If a GUI plugin ever lands, the service can't reach the desktop from there at all, so it usually ends up as a small helper running in the user's session that the service talks to over a named pipe. That helper's integrity level is where UIPI decides what it can click.

Thread Thread
 
_9439daaf10225d6b438d6 profile image
Junxiang Pan •

This is exactly the architecture I needed to hear. It makes total sense.

If a GUI plugin ever lands, I'll absolutely keep the service in Session 0 and use a separate helper in the user session, communicating over a named pipe. That cleanly separates the privileged service from the interactive desktop.

And the integrity level of that helper is the exact piece I'll have to be careful with. I'll make sure the helper logs an explicit UIPI_BLOCKED_GUI_INTERACTION event when a click is dropped, instead of failing silently and making users think the plugin is broken.

Right now, I'm keeping CBS strictly shell/plugin-only. The whole point is to lock down the service account. When the day comes that I need GUI control, I'll definitely reference this helper + named pipe pattern. Thanks for the masterclass!

Collapse
 
reidmarlow profile image
Reid Marlow •

Phone TOTP burns out very fast once an agent loop runs multi-turn tasks. When a model makes fifteen tool calls in a row, typing a six-digit code for every overwrite ends with the user turning the check off or blindly approving without reading the diff. A local confirmation socket or a scoped token tied to a specific scratch folder for thirty minutes holds up much better in daily use.

On the Windows primitives, Job Objects give you an immediate win before dealing with full AppContainers. Setting kill-on-close limits guarantees that subprocesses spawned by Python or cmd cannot orphan themselves when your broker drops.

Collapse
 
_9439daaf10225d6b438d6 profile image
Junxiang Pan •

You nailed the exact UX problem I'm trying to solve. The 15-call loop with a 6-digit code for every step is brutal, and you're right — users will just turn it off or blindly approve.

I just implemented a temporary "30-second bypass" in the dev branch to handle batch operations, but your idea of a scoped token tied to a specific scratch folder for 30 minutes is much better. I'm adding that to the roadmap.

Also, thank you for the Job Objects tip. The kill-on-close limit is exactly what I need for the 1.7 runtime sandbox. Right now, if the Broker crashes, orphaned child processes could escape the ACL. I'll be adding a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE to the Broker so the process tree is nuked when the Broker drops. Much appreciated!

Collapse
 
reidmarlow profile image
Reid Marlow •

The revert argument assumes the blast radius is pure code. Where that falls apart in practice is migrations, config files that sync to live infrastructure, and other agents that immediately branch off the new HEAD.

If I turn that on, the first rule is a separate test checkout: the test runner resets tests/ back to origin/main before running the suite, so the agent cannot edit its own grader into passing. The second is diff boundaries. If it touches anything outside an explicit whitelist of leaf packages, it stops for a human look regardless of whether CI went green.

Collapse
 
_9439daaf10225d6b438d6 profile image
Junxiang Pan •

This is a masterclass in workflow security. You're completely right—the blast radius isn't just the code; it's the CI pipeline and the infrastructure it syncs to.

The "AI editing its own grader" is a terrifyingly real failure mode. Your tests/ reset and diff boundaries are exactly how a proper pipeline should enforce integrity.

In fact, this is exactly why I built CBS to have an approval-only mode. I intercept the tool calls at the OS level. If the agent tries to write or delete inside a protected directory (like tests/ or config paths), the system pauses and forces a TOTP approval.

Even if the agent tries to bypass the plugin by spawning a subprocess, the Windows Job Object (KILL_ON_JOB_CLOSE) and strict ACLs will deny the write at the kernel level.

For my next update, I'm definitely looking into a "Diff Boundary" policy. If the file path isn't in an explicit whitelist of leaf packages, it hard-stops. Fail-closed on the filesystem level, not just the CI level. Thanks for pointing this out—it perfectly validates the need for OS-level enforcement.