Builtpublic

Permissions and guards

Opening an agent with shell access on your own PC to other people. And stopping is a feature.

3 tiers

An agent with shell access on your own PC is a different risk class from a chatbot. Which is why the longest part wasn’t the agent loop — it was the guards.

Tier Where What they get
Owner DM · every channel Everything — shell, files, skills
Org member Bound channels Only the folders named in that org’s file, read-only
Guest Invited channels Only allowlisted folders. Bash and Skill blocked

Deny reads and they read with the shell

Blocking Bash in the guest tier is the important half. Lock file reads and leave the shell open, and they read files with the shell. Skill execution and sub-task spawning are blocked for the same reason — one open indirect path makes closing the direct path meaningless.

There was a practical trap here. The deny rules grow to 468 entries. Passed on the command line, that hits the shell’s command-length limit and nothing runs at all. The error is a single line about the command being too long, which makes the cause hard to find. They go through a settings file now.

And an empty manifest means deny everything, not allow everything. If someone empties the file by accident, locking is better than opening.

An org is one file

One file, one org. Members, bound channels, readable folders, permitted commands, and an hourly cap all live in it.

Resolution order is fixed — owner, then explicit member, then channel binding, then unaffiliated. Orgs are isolated from each other, and org files are read-only.

They’re files because you have to be able to open one and look. If permission state only exists in a database or a command history, answering “what can this person see right now” means running a tool. With a file you just read it.

Credentials get reverted, and reported

Editing the config file directly gets reverted, with a notification to the owner.

Because a half-applied credential change is worse than none. An agent running half-switched blurs which account did what.

I got caught by this guard myself. I put a token in by hand, saw it gone later, and misdiagnosed it as “the save failed,” losing an hour. The revert sends a notification and I hadn’t looked at it. Confusion caused by the guard working correctly — so now “if it vanished, check the alert first” is step one.

Stopping is a feature

When resuming a long pipeline from a thread, Loki shows the options and what each one costs, then stops. Nothing runs until it’s approved.

Two properties matter here.

The gate never calls the brain. So it keeps working even when the subscription quota is exhausted. Being unable to confirm a resume in the exact situation where quota is why you can’t resume would be blocked twice over.

The pending state lives on disk. The first version held it in memory for fifteen minutes, and the very first real use broke it — asked at 15:27, answered at 16:53. Eighty-six minutes. The expired confirmation leaked quietly into the model, and the model invented an answer: “shall I run it manually?”

Fifteen minutes wasn’t wrong for being short. Resuming is something you do after waiting for a quota reset, so a lifetime measured in minutes never matched reality — and it had to survive a worker restart.

The risk of a stale answer isn’t handled with a clock. When the answer arrives, the options are recomputed and compared against what was originally asked — and if they differ, it asks again instead of running. Expired ones aren’t dropped silently either; they’re reported as expired.

Anything that leaves the machine

Publishing, pushing, filing tickets. All of it prepares automatically, then stops and waits for a human.

One criterion: does a trace end up outside this machine? Editing a file is reversible; a published post can be deleted, but somebody has already seen it.