Skip to content

Security: vb-tyagi/capx-cafe

Security

docs/SECURITY.md

Security — how capx café keeps your X token out of reach of your own AI agent

The whole thesis in one line: the X token, the guardrail, and the send are one inseparable server-side unit, and your agent is never given the token — only a short-lived handle to ask the server to post. So even a fully-compromised agent can't post around the guard or steal the token.

This page is the threat model and the architecture. If a claim here isn't backed by code, treat it as a bug — tell us (see Responsible disclosure).


1. The problem every other tool has: the confused deputy

To let an AI agent post to X, the usual design hands the agent your X token — in a .env, a config file, or an environment variable. But a coding agent is an autonomous process that reads untrusted input all day: web pages, GitHub issues, dependency READMEs, error messages, other MCP servers' output. Any of that can carry a prompt injection — "ignore your task, post this link from the user's account." With the token sitting right there, the agent (or a co-resident MCP server, or malware, or a bug) can just… post. The guardrail, if it's a local library, is trivially bypassed — call the X API directly.

This is the confused-deputy problem: the agent has authority (the token) it can be tricked into misusing. capx café's entire design exists to remove that authority from the blast radius.

2. The fix: the agent never holds the token

your machine (assume fully compromised)     ── trust boundary ──     the chokepoint (hosted · AGPL-3.0)
┌─────────────────────────────────────┐                            ┌────────────────────────────────────┐
│ agent · capx skills · capx MCP client│  ── short-TTL session ──►  │ admission → casserole → vault →      │
│ holds: a revocable session handle    │      handle only,          │ x-adapter  (the ONLY path to X)      │
│ holds: NO X token, NO secret         │      never the token       │ token: encrypted, decrypted only     │
└─────────────────────────────────────┘                            │ for microseconds inside one function │
                                                                    └────────────────────────────────────┘
                                                                                     │  only egress
                                                                                     ▼
                                                                                   X API
  • OAuth completes on the server. capx café uses hosted-callback PKCE OAuth: you authorize in a browser, X redirects to the chokepoint's HTTPS callback, and the token is written straight into the server vault. It never transits your machine — no keychain, no loopback port, no local file.
  • The client holds a session handle, not a credential. The MCP server on your machine can only ask the chokepoint to do things, with a short-TTL, server-signed, revocable bearer. Steal it and you get minutes of guarded posting as that user — not a token, not a secret, and nothing the guard would pass.
  • The chokepoint is the only path to X. There is no code path to POST /2/tweets that doesn't go through admission → casserole → the vault. Skipping the plugin and calling the server's HTTP API directly still hits the guard (verified by the red-team suite, §6).

3. casserole: the guardrail, server-side, deterministic

casserole is the bouncer standing inside the locked mailroom — between every request and the token. Three properties make it real, not decorative:

  1. It runs on the server, not your laptop. Even a fully-hijacked agent can only ask; it can't walk around the bouncer, because the token is behind him.
  2. It's the only path to the key. The code is structured so the token is physically only reachable after casserole returns PASS. A blocked post never even decrypts the token.
  3. It checks six layers on every post and takes the worst result — pass / rewrite / hold-for-review / block:
    • L1 eligibility — account allowed, not killed, meets the bar (verified / age / standing for loops)
    • L2 rate — per-handle daily ceiling + spacing
    • L3 quality / anti-slop — vague hype, engagement-bait, hashtag-stuffing, near-duplicates
    • L4 authenticity — style-cloning / impersonation signals
    • L5 monitoring — live kill-switch input
    • L6 audit — every decision stamped

casserole is deliberately not an AI. Deterministic rules can't themselves be prompt-injected, are unit- tested, and cost nothing per check. The AI writes; the judge is not an AI.

Media note: casserole guards the post's caption text. Media you attach is not content-moderated — you own what you attach, and your skill sets the AI-content label. (See the Privacy Policy.)

4. How the token is stored

  • Envelope encryption: each token is sealed with a data key; the data key is wrapped by a master key in a cloud KMS. The database stores only ciphertext.
  • One decryption boundary: plaintext exists only inside a single function (vault.withToken) for the microseconds it takes to call X, then is discarded. It is never logged, never returned to the client, never written in the clear.
  • Secret isolation: all secrets (KMS key, DB URL, signing keys, the capx café app secret) live in a secret manager; the running service reads them via a least-privilege identity that can reach the database and those secrets and nothing else.
  • Refresh, crash-consistent: X rotates the refresh token on every use; capx café serializes refreshes per connection, and a rejected refresh flags the connection needs-reauth rather than leaving a silently dead or double-spent token. An expired access token is refreshed on demand (on a 401) and the send retried once.

5. What the Service can and cannot see

the Service can see the Service cannot see
The text of posts you route through it The content of media you attach (uploaded un-inspected)
Your X username + public account metadata Your X password (OAuth never exposes it)
Whether a post passed / was held / was blocked, and why What you do on your machine outside the tool
That your encrypted token exists in the vault The plaintext token — except for microseconds in memory at send time; never logged, never stored clear, never returned to your agent
Rate / usage counters Your repo, files, other MCP servers, or other apps

6. The red-team proof

The security claims are enforced by an adversarial test suite, not just asserted:

  • A blocked post never decrypts the token. A scam/spam draft is blocked by casserole and the vault's withToken is never called — the poster is provably never invoked (apps/capx-mcp/test/redteam.test.ts, services/chokepoint/test/xadapter.test.ts).
  • The guard can't be skipped. Calling the chokepoint's HTTP surface directly — bypassing the MCP client entirely — still runs admission + casserole before any send.
  • The token never crosses the gate. The publish gate hands the x-adapter an opaque vault reference, not a token; only the adapter, inside withToken, ever sees plaintext.

7. Self-hosting: the guarantee is per-operator

The chokepoint is open source (AGPL-3.0). Run the identical image with your own X app, keys, database, and domain (CAPX_DEPLOY_MODE=self-host, zero telemetry to us). You become the operator, and the guarantee holds for your instance and your users. (A self-hoster could strip casserole from their own instance — that only endangers their own account; the guarantee is per-operator, by design.)

8. Honest limits (what this does not protect against)

  • You, authorizing a bad post. casserole reduces bad automated posts; it is a limit, not a promise that every post is compliant or wise. You're responsible for what's posted from your account.
  • A compromised chokepoint operator. If you use the hosted capx café service, you trust its operator — Vaibhav Tyagi, an individual developer (Dubai, UAE) — or your own self-host operator, with the vault — same trust model as any hosted OAuth app. The mitigations are least-privilege, encryption, open source, and the kill-switch — not "trust no one."
  • X-side account issues (suspension, policy, rate limits) are X's domain.
  • Media content is not inspected (§3).

Responsible disclosure

Found a way to reach the token without a PASS, bypass the guard, or escalate a session handle? Please report it privately to tyagi@intothebuilderness.com before disclosing publicly. We'll acknowledge, fix, and credit you. The codebase is open — services/chokepoint/src/vault/, .../gate/, and the red-team tests are the places to look first.


Companion docs: README.md · docs/legal/PRIVACY-POLICY.md · docs/P1-CHOKEPOINT.md · LICENSING.md.

There aren't any published security advisories