/deployHow Deploy works
Deploy sets one container's image on a Kubernetes Deployment and follows the rollout. You cast it from Slack or a terminal as /deploy followed by a Deployment name and image=repo:tag; a collector inside your cluster looks the tag up in your registry, then makes one strategic-merge patch of that container's image. It never deploys a mutable tag: ask for latest or a bare repo and it hands back the three most recent immutable builds to pick from, and it rejects a tag it cannot find before it touches the cluster. If your registry is unreachable, the deploy fails closed rather than guessing. The grant is get, list, and patch on Deployments plus read-only ECR describe access, so it can set an image and follow a rollout, and it can neither delete a workload nor push to the registry. Casting it needs deploy.deploy.cast, held outside the read grants, and by default it refuses any target that is not dev or staging.
Caster is pre-launch. This page describes how Deploy is built, not a product you can install today. The transcript figures come from the demo on this site, and they are labelled where they appear.
How it works
It never ships a moving target
Deploy takes an explicit image: image=repo:tag. Ask for latest, or a bare repo with no tag, and it does not deploy it; it replies with the three most recent immutable builds and lets you pick one, and each choice re-casts the deploy with that exact tag pinned. The tag convention it accepts is a git SHA, 7 to 40 hex characters, so the thing you ship is the thing a commit points at. It rejects a digest reference, whitespace, and a registry-qualified ref, so there is one way to name a build and it is the reproducible one.
The reason is the failure this prevents. latest is a name that moves, so deploying it means the image you shipped and the image you meant can differ by the time anyone looks. An immutable tag is the same image every time it is pulled, so a rollback is exact and an audit line means something.
The registry gate fails closed
Before it touches the cluster, Deploy looks the tag up in your registry. Today that is ECR, through a read-only ecr:DescribeImages call over IRSA, with no pull and no push. A tag that does not exist errors at preflight with the cluster untouched, so a typo is a failed cast, not a broken rollout. If the registry is unreachable, or the repository is not one the collector is scoped to, the deploy fails closed rather than proceeding on a guess.
The scope is explicit. SPELLBOOK_ECR_REPOS lists the repositories the collector may describe, and it is checked before any AWS call is made, so Deploy cannot go asking about a repository nobody put in front of it.
One patch, and the rollout it watches
Deploy resolves the Deployment, reads it, and picks the container to change: the only one if there is one, or the one you named with container=. A Deployment with more than one container and no container= is an error that names the containers, rather than a guess. It captures the current image as previous_image, then makes a strategic-merge patch of that one container's image, and records spellbook.dev/last-deploy-id and spellbook.dev/previous-image on the Deployment's own metadata, not the pod template, so those annotations do not themselves trigger a rollout.
Then it polls the Deployment every 2 seconds to a 300-second deadline, the same way Revivify does. Once the patch is durable, a timed-out, errored, or cancelled rollout reports that the image is patched and the rollout did not finish, degraded rather than failed, because the change landed and only the wait ran out. The final output prints a rollback line, rollback: image=repo:tag, built from previous_image; a rollback is just another deploy, gated the same way.
Production is refused by default, and the write is a separate grant
Deploy takes a target, and the default set is dev and staging. A production target is refused under any name, at preflight and again inside the invoke path, so the refusal cannot be raced. The policy is fail-closed: an unclear target gets the safe answer.
Casting Deploy needs deploy.deploy.cast, an entitlement outside the spell.* family, so holding read spells never adds up to a deploy. As with Revivify, there is no per-workload ownership: the entitlement, the collector's namespace scope, and the target policy decide what a cast can reach.
It runs in your cluster; the result comes back through the control plane
A collector runs inside your cluster and dials the control plane outbound over mTLS. The registry lookup and the patch both happen from inside; no kubeconfig, cloud credential, or shell leaves the cluster, and the control plane cannot widen the ServiceAccount or the IAM role, because both are scoped on your side. The deploy result, the image set and the rollout outcome, travels back through the control plane and is stored in the cast's row.
Setting it up
Setup happens once, run by whoever already administers the cluster. Everyone who casts a spell afterward holds none of what setup touches. What follows is the model set out in enough detail to check; nothing here runs today.
Run the collector in your cluster
The collector runs in its own namespace and dials the control plane outbound over mTLS. SPELLBOOK_NAMESPACES pins the namespaces it may patch, and SPELLBOOK_ECR_REPOS lists the registry repositories it may describe. Both are set here, on your side.
collector envSPELLBOOK_NAMESPACES=storefront,checkout SPELLBOOK_ECR_REPOS=checkout-api,cart-apiBind the deploy grant
Two grants, one in each system. In the cluster, get, list, and patch on Deployments. In AWS, ecr:DescribeImages through IRSA and nothing else: no pull, no push. Neither can be widened from the control plane.
deploy grants# kubernetes: rules: - apiGroups: ["apps"] resources: [deployments] verbs: [get, list, patch] # aws, via IRSA: # ecr:DescribeImages (read-only; no pull, no push)Grant the entitlement, pin the targets
spell.deploy.cast puts the spell in reach; deploy.deploy.cast is the write entitlement, held outside the spell.* family. The target allowlist defaults to dev and staging, and production is not on it.
entitlementsspell.deploy.cast deploy.deploy.cast # targets: dev, staging (prod refused under any name)Cast it
Ana ships an immutable build to staging. The tag is a git SHA, the registry is checked before the cluster is touched, and she holds no kubeconfig.
slack/deploy checkout-api image=checkout-api:9f3c1a2 --target staging
This is the design, not an install guide. The collector bundle, the flags, and the exact manifests are not something you can apply today.
What it never receives
The list a security review asks for, on the page rather than in a PDF behind a form.
- No kubeconfig and no cluster credential. The collector's ServiceAccount stays in the cluster, and you are never issued one.
- No shell. Deploy's grant has no exec, attach, or port-forward, so there is no path from a cast to a prompt inside a pod.
- No registry write. The ECR access is ecr:DescribeImages only, so Deploy reads which tags exist and can neither pull an image nor push one.
- No delete, no scale. The cluster grant is patch on Deployments, so Deploy sets an image and cannot remove or resize the workload.
- No mutable tag. latest and a bare repo are refused before the cluster is touched; only an immutable git-SHA build ships.
What it refuses to do
A permission that was never issued cannot be misused, by a person, by a compromised account, or by an agent.
- /deploy checkout-api image=latest
- Refused, and answered. Deploy does not ship a moving tag, so instead of deploying latest it hands you the three most recent immutable builds to pick from, each re-casting with that exact git-SHA tag pinned. The catalog excludes untagged and latest-only images, so the picker only ever offers something reproducible.
- /deploy checkout-api image=checkout-api:typo9
- Errors at preflight, with the cluster untouched. Deploy looks the tag up in the registry before it patches anything, so a tag that does not exist is a failed cast, not a half-finished rollout.
- /deploy checkout-api image=checkout-api:9f3c1a2 --target prod
- Refused. The target allowlist is dev and staging, and production is off it under any name, at preflight and again inside the invoke path. There is no override flag.
- Deploy a multi-container Deployment without saying which
- Errors and names the containers. Deploy changes one container's image; when a Deployment has more than one and you did not pass container=, it will not guess which to patch.
- Push or pull from the registry
- Deploy's registry access is ecr:DescribeImages and nothing else. It reads which tags exist; it cannot pull an image or push one, so a deploy cannot become a supply-chain write.
- Deploy while the registry is down
- Fails closed. No catalog binding, or a registry that does not answer, means the deploy does not proceed. Deploy would rather refuse than ship a tag it could not verify.
The audit record
A deploy is a write, and every cast is a casts row before the patch is sent. The durable record is that row. A cast refused at preflight, a bad tag, a prod target, a missing entitlement, never becomes a row: it is logged as a warning, so a queryable casts row always describes a deploy that actually ran.
cast_id cst_7b40aa19
tenant_id tnt_northwind
principal okta|00u1a4h2 (oidc subject, not an email)
castable deploy
idempotency_key slack:U03QK4T2P:1753281731
state succeeded
degraded false
started_at 2026-07-23T15:02:11Z
finished_at 2026-07-23T15:02:59Z
result { surface: slack #checkout-oncall,
target: checkout-api / storefront, env: staging,
container: checkout-api,
previous_image: checkout-api:7e12b0d,
image: checkout-api:9f3c1a2, rollout: done }Where a record lands
- The casts row in the control plane's database, isolated per tenant with row-level security. This is the durable record, and it is the one a query runs against.
- The tenant's Slack audit_channel, which mirrors the result of every terminal cast, so a channel of record exists even for casts nobody ran in Slack.
- The collector's stdout as structured JSON, including the previous image and the tag it resolved, so your log pipeline picks it up with no new integration.
- Your API server's own audit log records the collector's patch independently, and the cast id ties the two accounts together.
Failure modes
What happens when it cannot do the thing.
- The tag does not exist in the registry
- Deploy errors at preflight and patches nothing. The registry lookup runs before the cluster is touched, so a typo fails the cast cleanly instead of starting a rollout that cannot succeed.
- The rollout times out after the patch
- The image is already patched, so Deploy reports that the image is set and the rollout did not finish in 300 seconds, marked degraded rather than failed. The change landed; only the wait ran out, and the rollback line is in the output if you want to revert.
- The Deployment has more than one container
- Deploy will not guess. It errors and names the containers, so you re-cast with container= naming the one to change.
- The registry is unreachable
- The deploy fails closed. Deploy verifies the tag before it patches, so a registry that does not answer stops the cast rather than letting it ship an unverified image.
- You need to undo a deploy
- The previous image was captured before the patch, and the output prints it as a rollback line. A rollback is just another deploy, gated by the same tag rule and target policy, so undoing is as auditable as doing.
Questions
How do I deploy a new image to Kubernetes without kubectl access?
Cast /deploy followed by the Deployment name and image=repo:tag, from Slack or a terminal. A collector inside your cluster looks the tag up in your registry, picks the container to change, and makes one strategic-merge patch of that container's image, then watches the rollout. You never hold a kubeconfig, cloud credential, or shell, and the collector's grant is patch on Deployments plus read-only ECR describe access, with no registry write.
Why won't Deploy ship the latest tag?
Because latest is a name that moves. The image behind it can change without the tag changing, so a deploy of latest ships whatever the tag points at that second, and a rollback cannot be exact. Ask for latest or a bare repo and Deploy hands you the three most recent immutable builds instead, tagged by git SHA, and each pick re-casts with that exact tag pinned.
What does Deploy do if I give it a tag that doesn't exist?
It fails at preflight, with the cluster untouched. Deploy looks the tag up in the registry before it patches anything, so a tag that does not exist is a failed cast rather than a rollout that starts and cannot finish. The same holds if the registry is unreachable or the repository is not one the collector is scoped to: Deploy fails closed rather than guessing.
What permissions does Deploy hold?
In the cluster, get, list, and patch on Deployments. In AWS, ecr:DescribeImages through IRSA, read-only, with no pull and no push. There is no delete or scale verb, and the registry access cannot write. Casting it also needs deploy.deploy.cast, an entitlement held outside the read grants, and the target allowlist refuses production by default.
How do I roll back a deploy?
Deploy captures the current image as previous_image before it patches, and prints a rollback line in the output: rollback: image=repo:tag. Running that is just another deploy, gated by the same immutable-tag rule and the same target policy, so a rollback is as checked and as audited as the deploy it undoes.
Can an AI agent deploy?
Yes, as its own principal. Grant it spell.deploy.cast and deploy.deploy.cast on the targets it operates and it can ship an immutable build to them; withhold deploy.deploy.cast and it can hold every read spell and still deploy nothing. The write is a separate entitlement, so what an agent can change is exactly what you granted, and production is refused by default regardless.
The machine-readable version of this page is at /spells/deploy.md. It is generated from the same source, so it says the same thing.
Run this in your own cluster.
Caster is not open yet. Leave an address and we will come to you first.