August 22, 2026
The Token Was Hiding a Broken Gate
Ripped the bearer token out of the MCP server and leaned on the same permission gate as everything else — then spent the day finding the gate had real holes to fix.
The MCP server used to hand out a token at boot and call that security. Today it stopped. Access is now decided per tool, by the same Allow/Ask/Deny policy that already governs every other external call Chalie makes — and deleting the token is what forced that policy to actually work.
The token never did much. Every tool the MCP server exposes was already gated individually by that policy, so the token was a second, redundant check in front of a check that already existed. On upgrade, the old token rows get deleted and the credentials they minted get revoked, once. Turning the server on or off, or moving its port, now takes effect immediately instead of needing a restart — and a hand-edited port outside 0-65535 gets reported as a settings error instead of taking down the reconcile loop every five seconds. Wrote feature tests that hit the actual handshake with no credential, a stale one, a disabled server, a taken port, a bad port. A boundary you haven’t tried to break isn’t proven, it’s just untested.
Deleting the token meant Ask had to stop being decorative. An external agent calling in has no chat window to answer from, and a gated ask on that path used to have nowhere solid to land. A gated ask now names the exact turn that raised it — main spine, a specific thread, a scheduled run — and the prompt card lands there instead of a generic queue. A turn with no surface at all gets an immediate, explicit deny, logged as blocked, telling the model to go ask the user directly instead of retrying into a wall. The policy screen got one line above the table spelling out what Ask means on a channel nobody’s watching — the missing case needed a name instead of a hidden default.
Chasing that down surfaced real bugs in the part a human actually looks at. Cards were sometimes rendering in the spine when their turn actually lived inside an open thread panel, or the other way round — a card now follows its turn’s real surface and moves when the panel opens or closes. A failed Allow or Deny used to just drop the card; now it re-reads whether the gate is still open before deciding, so a reconnect mid-decision doesn’t erase something the user still needs to answer. Deleted a dead static entry point into the policy check that nothing called anymore, and its mocked test with it.
One unrelated fix rode along. Creating or copying a skill was returning a 500 after the skill, its search index, and its YAML were already written to disk — the response builder was failing on the way out, so the UI showed nothing and a retry hit a duplicate-name error on a row that already existed. Built the response from what the database actually stored instead.
An external agent’s actual permissions haven’t changed — the tool policy already decided what it could touch. What changed is that the policy is now the only thing in the way, no token backing it up, and today’s fixes read like a list of every place it was quietly getting the answer wrong. Next test is real traffic: an external agent hitting the server with nothing in its pocket but the policy itself.
-
Dropped the MCP server’s inbound bearer token entirely — access is now decided per tool by the existing external-agent policy, with no credential generated, stored, or shown
-
Server enable/disable and port changes take effect immediately, no restart; an out-of-range port is reported as a settings error instead of crash-looping the reconcile check
-
A gated “ask” now names the turn that raised it and lands on the right spine, thread, or schedule panel; a turn with no answerable surface gets an immediate, explicit deny instead of parking forever
-
Fixed permission cards rendering in the wrong lane relative to an open thread panel, and a failed Allow/Deny dropping a card that was still actually pending
-
Fixed skill create/copy returning a 500 after the row was already written to the database