About Enoki
Identity-aware agentic ChatOps for Google Cloud — manage GCP from Slack, as yourself.
Inspiration
ChatOps for the cloud has always made the same bad trade. To let a Slack bot "do things" in Google Cloud, someone gives the bot a service account with broad permissions. From that moment on, everyone in the channel acts with the bot's identity, not their own. Access control quietly collapses to "can you type in Slack," every Cloud Audit Log entry shows one robot doing everything, and a single leaked bot token becomes a leaked cloud.
We kept running into this. The convenience of "just ask in Slack" is real and worth chasing — but the security model underneath it is a liability nobody wants to own. We wanted the convenience without the trade. The question that started Enoki was simple: what if the bot never had any privileges of its own, and every action ran as the actual human who asked for it? If we could preserve identity end-to-end, then IAM, audit logs, and accountability would all just keep working the way they were designed to.
That single principle — identity preservation — is the spine of the whole project.
What it does
You add Enoki to Slack, link your own Google account once from the App Home tab, and then you just talk to it in plain English:
"List my stopped VMs," "who has owner on the billing project," "how much did we spend on BigQuery last week," "roll cloud-run
apiback to the last good revision."
An LLM agent plans the request and a catalog of 71 typed Google Cloud tools across 20+ services does the work. Three rules make it safe:
- You act as yourself. Every BigQuery query, VM start, and IAM read runs under your OAuth credentials. GCP's own IAM decides what you can do; Enoki adds no privileges. Cloud Audit Logs show the real human, not a bot.
- Writes are a decision, not a side effect. Reads stream back inline. Any mutating action pauses the agent and posts an Approve / Deny card showing exactly what will run — and the approval is cryptographically bound to that exact call. The highest-risk IAM grants additionally require a second, distinct approver.
- Everything is on the record. An append-only audit log captures who ran which tool, with what arguments and what result.
It works in the Slack Assistant panel, DMs, channels, and @mentions; ships
/enoki <question> plus 21 deterministic /gcp_* slash commands; exposes its
tools as no-code Workflow Builder steps; and can pull in the Slack MCP server
when your user token allows. There is deliberately no free-form command tool —
no run_gcloud, no raw SQL exec.
How we built it
- The agent. A single Pydantic-AI
Agentrunning Gemini on Vertex AI does the reasoning. Itsoutput_typeis[str, DeferredToolRequests], so a write tool doesn't execute — it pauses the run and emits a deferred request that the Slack layer turns into an approval card. On approval, the run resumes from exactly where it paused. - The tools. 71 plain, typed Python functions under — Compute, Storage, BigQuery, IAM, Cloud Run,
Pub/Sub, GKE, Cloud SQL, Spanner, logging, monitoring, and more. Read tools run
inline; write tools are wrapped
Tool(fn, requires_approval=True). The type boundary is the guardrail: no tool accepts a raw command, shell string, or raw SQL. - Plan binding. hashes the canonical
{tool_name, args}of the approved plan with SHA-256. Execution recomputes the hash and aborts on any drift — so an approval can only ever run the precise call it was shown. - Two identities, never crossed. Inference uses the app's own least-privilege service account (Vertex AI + shared state only, no data-plane access). Every GCP data-plane call uses the connecting user's Fernet-encrypted OAuth credentials. The two never mix.
- The surface. Slack Bolt for Python (events, commands, actions, views, workflow functions), a Flask + OAuth entrypoint for multi-user hosting and a Socket Mode entrypoint for local dev.
- State & delivery. Firestore (Native) holds conversations, pending approvals, idempotency records, and the audit trail, with SQLite / in-memory backends for smaller setups. Results export to the thread as CSV/PDF.
- Deployment. A single deploy provisions APIs, Firestore, a state bucket, a least-privilege runtime SA, and Secret Manager entries, then builds and deploys to Cloud Run. A separate SvelteKit + Tailwind marketing site ships to Vercel.
Challenges we ran into
- Approvals that survive a stateless world. Cloud Run runs N instances, so the Approve click almost never lands on the instance that proposed the write. We had to make the agent run resumable across processes — persisting the paused run in Firestore and rehydrating it on whichever instance handles the button, all inside Slack's tight interaction deadlines.
- Binding an approval to a plan. "Approve" has to mean this exact call and nothing else. Hashing the canonical tool call and re-checking it at execution time — voiding on drift, expiry, or reuse — was fiddly to get right, especially keeping argument serialization stable enough that an honest plan never false-aborts.
- Keeping two identities strictly separate. It's dangerously easy to let a
tool fall back to Application Default Credentials and silently run as the bot.
We enforced a hard rule — inference uses the app SA, every data-plane call uses
the user's OAuth creds from
AgentDeps— and structured the code so the wrong path isn't reachable. - Making BigQuery safe without crippling it. Regex can't reliably tell a
SELECT from a hidden mutation. We parse the SQL with
sqlglotto enforce SELECT-only, and dry-run a byte estimate against a cost ceiling before anything is billed. - Separation of duties. Detecting privileged IAM grants
(
roles/owner,roles/editor,roles/iam.*,roles/resourcemanager.*) and requiring a second, distinct approver — so one person can't both request and rubber-stamp their own escalation. - Auditing nothing gets missed. Rather than instrument every tool (easy to
forget one), we derive the audit trail by walking a finished run's new messages,
tagged with a
sourcemarker.
Accomplishments that we're proud of
- A real, deployed, multi-user app — not a demo. Enoki runs on Cloud Run
with a per-user OAuth account-linking flow, Firestore-backed state, and an
idempotent one-command deploy. It works in the Assistant panel, DMs, channels,
@mentions, slash commands, and Workflow Builder. - 71 typed GCP tools across 20+ services. Compute, Storage, BigQuery, IAM, Cloud Run, Pub/Sub, GKE, Cloud SQL, Spanner, Memorystore, logging, monitoring, Scheduler, Tasks, Build, Artifact Registry, VPC/firewall, Cloud DNS, billing — each a named, typed function with reads inline and writes gated.
- A security model that actually holds. No shared god-mode identity; every action runs as the human who asked. Approvals are cryptographically bound to the exact call, privileged IAM grants demand a second distinct approver, and a compromised app service account still can't read your cloud.
- Cross-instance resumable approvals. The agent run pauses on a write and resumes correctly even when the Approve click lands on a different Cloud Run instance — durable state, inside Slack's interaction deadlines.
- Guardrails by construction, not by patching. No free-form execution tool,
sqlglot-parsed SELECT-only BigQuery with dry-run cost estimates, Fernet- encrypted refresh tokens, signed single-use OAuth state, and an append-only audit trail derived automatically from each finished run. - Two identities that never cross — app SA for inference only, user OAuth for every data-plane call — enforced so the wrong path isn't reachable.
What we learned
- The security win comes from what you remove. Deleting the shared god-mode service account — not adding features — is what makes the model sound. A compromised Enoki SA can't read your cloud, because it has no data-plane access at all.
- Types are a security boundary. By making every capability a named, typed Pydantic tool and refusing any free-form execution tool, we got safety from the interface itself instead of from runtime string-scrubbing.
- Deferred / resumable agent runs are a genuinely powerful pattern. Treating human approval as a first-class pause-and-resume point — not a bolted-on callback — let the same mechanism serve slash commands, the Assistant panel, and Workflow Builder steps.
- Stateless infra forces honest state design. Multi-instance Cloud Run meant we couldn't hide anything in process memory; every bit of conversation and pending-approval state had to be explicit and durable, which made the whole system easier to reason about.
- Identity preservation answers the one question a shared-credential bot never can: who actually did this, and were they allowed to?
What's next for Enoki
The shipped app covers per-user identity, human-approved writes, and full auditing on a single Cloud Run service. Next, we want to harden it into a Marketplace-ready, multi-tenant platform:
- Short-lived per-user credentials. Move from stored OAuth refresh tokens to per-user service accounts in a dedicated identity project, with tokens minted on demand via Workload Identity Federation (TTL ≤ 10 min, no long-lived keys).
- A deterministic policy engine. A classification layer
(
read | mutate | destructive | high_cost) driven by a per-tenantrules.yamlthat authorizes every proposed tool call — the LLM proposes, policy decides — extending two-party approval to all destructive actions on prod-labeled projects. - Just-in-time access (Gatekeeper). Time-bound IAM Condition bindings with manager approval from the org directory graph, plus reconcilers that revoke on expiry and deprovision service accounts for suspended or removed users.
- Infrastructure provisioning (Builder). Draft and apply Pulumi changes behind a CrossGuard policy gate, so a policy-violating plan never reaches an Approve button.
- Split ingress/worker services. A public ingress (verify signature + timestamp + replay nonce, ack < 3s) decoupled from an internal worker via Pub/Sub, so the privileged path is never public and Slack's 3s deadline is never blown by a cold start.
- Zero-copy Slack reads. Add the Real-time Search API alongside the MCP server for user-scoped context, storing only references, never message text.
- Marketplace readiness. Directory-API identity resolution on immutable
subject IDs and strict
tenant_idisolation on every state access.
Built with
Python · Slack Bolt for Python · Pydantic AI · Gemini on Vertex AI · Google Cloud
(Compute, Storage, BigQuery, IAM, Cloud Run, Pub/Sub, Firestore, Secret Manager,
Cloud Asset Inventory) · sqlglot · Fernet · Flask/gunicorn · Cloud Run ·
SvelteKit + Tailwind (marketing site) · Vercel.
Built With
- fernet
- flask
- gcp
- gemini
- google-cloud
- gunicorn
- pydantic
- python
- slack
- slack-bolt-python
- slack-mcp
- sveltekit
- tailwind
- vercel
Log in or sign up for Devpost to join the conversation.