Skip to content

feat(centinela): capability firewall with MCP gateway and WhatsApp step-up - #462

Open
JacoboCamargo wants to merge 14 commits into
gethouston:mainfrom
JacoboCamargo:feat/centinela-capability-gate
Open

feat(centinela): capability firewall with MCP gateway and WhatsApp step-up#462
JacoboCamargo wants to merge 14 commits into
gethouston:mainfrom
JacoboCamargo:feat/centinela-capability-gate

Conversation

@JacoboCamargo

Copy link
Copy Markdown

PR: Centinela, a capability firewall for Houston agents

What this adds

Centinela is a deterministic capability firewall for Houston agents. The LLM is
a confused deputy, not a security boundary: this moves the trust decision out of
the prompt and into code the model cannot bypass.

Two new engine crates (frontend-agnostic, no Tauri, no React):

  • engine/houston-centinela — the Policy Core. Pure logic. evaluate()
    returns Allow | Deny | StepUp over capability scopes, taint tracking, an
    egress allowlist, the Rule of Two, step-up capabilities and a duress latch.
    Every DENY is checked before any STEP_UP, so the gate is fail-closed by
    construction. 25 tests.

  • engine/houston-centinela-mcp — an MCP gateway (JSON-RPC over stdio) that
    wraps the core. Every tool call the agent makes is gated before it runs; the
    model only ever sees this endpoint. Verdicts stream to a journal a plain
    language Salvoconducto UI tails. A STEP_UP escalates to the owner over
    WhatsApp (Meta Cloud API) and waits for SI/NO; deny and timeout are
    fail-closed. 17 tests.

Why

The same artifact serves both hackathon tracks: a plain-language permission
passport for non-technical users, and a code-enforced boundary for safe deploy.

Grounded in CaMeL (Google DeepMind) and the Agents Rule of Two (Meta);
blocks the Lethal Trifecta (Willison) by structure, not by filters.

Conventions followed

Fail-closed default DENY, typed decision reasons (enums with Display), no silent
failures (every verdict surfaces), files under 200 lines, no em dashes in
user-facing copy, secrets only via env vars.

Tests

cargo test -p houston-centinela -p houston-centinela-mcp — 42 passing.
cargo clippy clean, cargo fmt clean.

How to try it

See engine/houston-centinela-mcp/README.md (gateway) and WHATSAPP-SETUP.md
(the WhatsApp step-up demo). cargo run -p houston-centinela --example demos
runs the three blocking demos with no external setup.

santoriaproducts-design and others added 14 commits June 6, 2026 12:12
…ep-up

Centinela is a deterministic capability firewall for Houston agents. The LLM is
a confused deputy, not a security boundary: this moves the trust decision out of
the prompt and into code the model cannot bypass, no matter how persuasive the
input.

- engine/houston-centinela: the Policy Core, pure logic. evaluate() returns
  Allow | Deny | StepUp over capability scopes, taint tracking, an egress
  allowlist, the Rule of Two, step-up capabilities and a duress latch. Every
  DENY is checked before any STEP_UP, so the gate is fail-closed by
  construction (deny beats step-up beats allow). 25 tests.

- engine/houston-centinela-mcp: an MCP gateway (JSON-RPC over stdio) wrapping
  the core. Every tool call the agent makes is gated before it runs; the model
  only ever sees this endpoint, so it cannot reach the tools except through the
  gate. Verdicts stream to a journal the Salvoconducto UI tails. A STEP_UP
  escalates to the owner over WhatsApp (Meta Cloud API template) and waits for
  SI or NO; deny and timeout both block. 17 tests.

Grounded in CaMeL (Google DeepMind) and the Agents Rule of Two (Meta).
Follows Houston conventions: fail-closed default DENY, typed decision reasons,
no silent failures, files under 200 lines, no em dashes in user-facing copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The approval recipient is the trust anchor for the whole step-up channel: if any
number could be set, an attacker points it at their own phone and self-approves.
A number is now accepted only after a one-time code we send to it is echoed back,
and the verified anchor lives server-side where the agent can never reach or
change it.

- enrollment.rs: OTP store, single-use codes with a TTL, the verified anchor.
- the WhatsApp client now sends to an explicit `to`: approvals go to the
  verified number, codes go to the number being verified (WHATSAPP_OTP_TEMPLATE
  or free-form text inside the 24h window).
- the webhook gains POST /enroll/start and /enroll/confirm, plus permissive CORS
  so the Salvoconducto UI can call them.
- the UI gains a "verify your number" panel.
- no verified number means every step-up is fail-closed.

23 tests, clippy clean, fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redo the Salvoconducto from a dark theme to Houston's light, ChatGPT-like look
(near-black on white, monochrome with semantic accents, invisible borders,
visible actions) per knowledge-base/design-system.md: rounded-full buttons,
rounded-xl cards, soft semantic pills (success/warning/danger), clean type. The
number-verification panel and the live decision log are included; layout is
responsive. Rendered states verified by headless screenshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…empts

The gateway is the choke point every sensitive call already passes through. The
Auditor reviews each verdict and, on a real bypass attempt (a jailbreak for an
undeclared capability, the duress latch, or exfiltration via taint/egress),
alerts the verified owner out of band over WhatsApp. Step-ups (handled by the
approver) and allows raise no alert; fail-closed when no number is verified.

Outbound messaging now sits behind a `Notifier` trait, so the whole approval,
enrollment and audit flow is exercised against a `MockNotifier` with no network.

- notifier.rs: the `Notifier` trait + a recording `MockNotifier` for tests.
- auditor.rs: the Auditor and its bypass classification.
- whatsapp.rs: implements `Notifier`, adds `send_alert` (+ optional
  `WHATSAPP_ALERT_TEMPLATE`).
- server.rs: `Hooks { approver, auditor }`; the gateway audits every verdict.
- approver/webhook/main wired through `Arc<dyn Notifier>`.

36 tests (mock-backed: approver yes/no/timeout/send-failure, auditor per bypass
type + allow/step-up/no-verified, gateway integration). clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Houston agent (houston.json + CLAUDE.md) that runs behind Centinela: reads and
drafts flow freely, irreversible actions require WhatsApp approval, and
undeclared capabilities are blocked and alerted. Seeds its own capabilities.json
salvoconducto. ShieldCheck icon, Spanish copy, no em dashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demo tools now read from a believable profile (inbox with a poisoned email,
bank balance, transactions) so the whole flow is visible end to end.
demo-flow.sh runs the six-step sequence through the gateway over one session
(taint persists), narrates it, and populates the Salvoconducto UI's live log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a real "Salvoconducto" tab to the Houston desktop app (STANDARD_TABS +
tab-resolver + a React component using the design-system tokens). It shows the
agent's permissions in plain language and a live decision log polled from the
gateway's new GET /decisions endpoint. Typecheck passes (pnpm tsc).

- app/src/components/tabs/salvoconducto-tab.tsx: the tab component.
- app/src/agents/{standard-tabs,tab-resolver}.ts: register the tab.
- engine/houston-centinela-mcp/src/webhook.rs: GET /decisions (journal as JSON).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e toggle

Even a permitted action can leak. An allowed email may carry an API key, a
private key, a card number or a password. The new content-inspection layer scans
the outbound payload and blocks the send when a secret is found, regardless of
whether the capability is granted: not "what can the agent do" but "what is it
taking out". It is a per-agent toggle the owner flips live, and a blocked leak
also raises an Auditor alert.

- houston-centinela: secrets.rs scanner (API key, AWS key, private key, JWT,
  card via Luhn, password), Reason::SensitiveContent, Session.inspect_content,
  ToolCall.payload, and the evaluate() check. 12 new tests.
- houston-centinela-mcp: a shared Arc<AtomicBool> toggle, webhook GET /inspect +
  POST /toggle/inspect, the gateway syncs it into the session per call, and the
  Auditor alerts on a blocked leak.
- the Salvoconducto UI (standalone + the native Houston tab) gain the switch.

73 tests, clippy + fmt clean, pnpm tsc passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing)

In a multi-agent system agents inevitably talk to each other, and that channel
is a leak path: a poisoned email agent asks the bank agent for the accounts and
mails them out. Centinela cuts it at the source: agent-to-agent relay is modeled
as egress, so an agent can read sensitive data but cannot export it to an agent
that is not a cleared destination. Access is not export. A tainted session
cannot relay at all (taint -> egress), and even a cleared relay asks the human.

- relay_to_agent tool mapped to capability agent:relay (is_egress).
- demo salvoconducto declares agent:relay, allowlists only cleared agents,
  requires step-up for it.
- 3 tests covering the data-laundering attack. 76 tests total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sApp creds

The content-inspection toggle and /decisions only worked when WhatsApp
credentials were set, because the webhook spawned inside the approver branch.
The webhook now runs whenever the gateway runs, with an optional notifier: the
UI endpoints (/inspect, /toggle/inspect, /decisions) always work, while the
reply and enrollment endpoints return 503 without credentials.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Salvoconducto tab now lets the owner verify their approval number in-app
(number -> code -> verify), calling the gateway's /enroll endpoints, matching
the standalone UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oconducto

Each capability is now a switch the owner flips from the Salvoconducto UI:
revoke a permission and the gate denies it on the next call, grant one and it
takes effect immediately, no restart. The base salvoconducto stays immutable;
live overrides are applied on top per verdict.

- houston-centinela: Capabilities::set_capability (grant/revoke), + test.
- houston-centinela-mcp: shared Arc<Mutex<HashMap>> overrides in ServerState,
  effective_caps() applied per verdict, webhook GET /permissions +
  POST /toggle/permission. The webhook deps are bundled in a public Web struct
  instead of a growing positional argument list.
- the Salvoconducto UI (standalone + native Houston tab) render a switch per
  permission, reading /permissions and posting toggles.

79 tests, clippy + fmt clean, pnpm tsc passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single terminal command now drives the whole step-up flow: POST /demo/request
sends the WhatsApp approval to the verified owner and blocks until they reply SI
or NO, returning the outcome. Approver::with_registry lets the HTTP-triggered
request share the webhook's registry, so the same reply resolves it. The
decision is journaled so the Salvoconducto UI shows it. pedir-permiso.sh wraps
the curl with friendly output.

42 gateway tests, clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings up the whole demo from a single command: builds and starts the gateway,
the Salvoconducto UI, the cloudflared tunnel, and (given META_APP_ID/SECRET)
points the Meta webhook at the fresh public URL. Makes the demo reproducible
without hand-running each piece.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants