/scryHow Scry works
Scry reads recent logs for a Kubernetes service. You cast it from Slack or a terminal as /scry followed by a service name; a collector inside your cluster resolves the name to the pods behind it and reads the last 20 lines of each, under a ServiceAccount that holds get and list on pods and pods/log and nothing else. It does not read events or metrics, and it does not query Prometheus. It does not diagnose. It cannot write, restart, scale, or delete, and it cannot read a Secret, because none of those verbs or resources are in the grant. The log lines it returns are the cast's result, and they are stored in the control plane, unredacted, so a scry is exactly as sensitive as the logs it read.
Caster is pre-launch. This page describes how Scry 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 reads pod logs, and only pod logs
A scry resolves the service name to the pods behind it, then reads the last 20 lines of each pod's logs. That is the whole read. It does not pull Kubernetes events, and it reads nothing from metrics.k8s.io or a Prometheus. If you want to see what a service is printing right now, this is the spell; if you want a cause, that is a different spell with a different grant.
Name resolution is loose on purpose, because nobody types the exact object name mid-incident. Scry matches the service label first, then falls back to a case-insensitive substring over pod names and labels across the namespaces it is scoped to. It caps the read at 10 pods and fetches them at once. When a pod runs more than one container it follows the kubectl.kubernetes.io/default-container annotation, and it skips istio-proxy, so you get the application's log rather than the mesh sidecar's.
It runs in your cluster; the result comes back through the control plane
A collector runs inside your cluster. It dials the control plane outbound over mTLS and does every read in place, under a Kubernetes ServiceAccount your team scopes. Nothing dials in: no ingress, no public URL, no inbound firewall rule. No kubeconfig, cloud credential, or shell ever leaves the cluster, and the control plane cannot widen what that ServiceAccount can do, because the namespaces and the RBAC that bound it are set on your side.
The log lines themselves do leave. A cast's result travels back through the control plane and is stored there as JSONB, isolated per tenant with row-level security and a bounded retention window. Most cast results default to private; scry marks its result shared, so a successful answer posts in the channel you cast it from. There is no redaction step. Pod logs routinely carry secrets, so a scry result is as sensitive as the logs behind it, and it should be handled that way.
It resolves, or it asks
When the name matches nothing, Scry replies with the services it can see in scope and casts nothing. That reply is private to you, not posted to the channel, because a list of service names is not something to broadcast by accident.
When one pod's log fetch fails while the others succeed, the reply is marked degraded and returns what it got.
Who is asking, and what a scry cannot become
Identity comes from your own IdP over OIDC. In the terminal, a device-flow sign-in caches a short-lived access token and a refresh token at file mode 0600 and refreshes them silently as they expire; from Slack, Socket Mode maps your Slack user to a subject and the roles it holds. The collector holds the ServiceAccount that reads the logs. You never do.
Read-only here is not a promise in our code; it is the absence of a verb. Scry's grant is get and list on pods and pods/log. It holds no write verb and no exec, and Secrets are not a resource in it, so a scry cannot restart the thing it just read and cannot fall through to reading a Secret even if the code tried. Your API server enforces that ceiling.
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 in enough detail to check; nothing here runs today.
Run the collector in your cluster
The collector is deployed into its own namespace with its own ServiceAccount. It needs no inbound networking, because it opens the connection to the control plane itself, outbound over mTLS. SPELLBOOK_NAMESPACES pins the namespaces it may ever touch, and it is set here, on your side, where the control plane cannot change it.
collector envSPELLBOOK_NAMESPACES=storefront,checkoutBind the read grant
Scry's ServiceAccount gets one RBAC rule, and it is the hard ceiling. Whatever the control plane asks for, the API server rejects anything this rule does not allow. There is no write verb in it and no Secrets, so no scry, and no bug behind one, can restart a workload or read a Secret.
scry ClusterRolerules: - apiGroups: [""] resources: [pods, pods/log] verbs: [get, list]Grant the entitlement
In the control plane, a role carries the string spell.scry.cast, and that string is what lets a given person or agent cast /scry. The match is exact unless a grant ends in .* , so spell.scry.cast grants scry and nothing near it. This layer decides who may cast; the RBAC rule above decides what a cast can reach.
entitlementspell.scry.castCast it
Ana signs in as herself through your IdP and types this in the channel she was already in. She holds no kubeconfig, and she never gets one.
slack/scry checkout
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. Scry's grant has no exec, attach, or port-forward, so there is no path from a cast to a prompt inside a pod.
- No Secrets. core/v1 Secrets are not a resource in the grant, so a scry cannot read one even when that would be the shorter path to an answer.
- No write verb. The grant is get and list, so a scry cannot restart, patch, scale, or delete anything it read.
- No cloud credential. Scry reads pod logs; it holds no IAM role, cloud key, or registry access.
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.
- kubectl logs -f checkout-api
- kubectl was never issued to you, so there is nothing to revoke. The spell is the interface, so the raw version of this read, the one you can point at any namespace, does not exist for you to get wrong.
- Restart the thing it just read
- Scry holds no write verb. It cannot patch, scale, restart, or delete. The read ends at the read, and restarting is a different spell with its own grant.
- Read a Secret, or the env var that holds one
- core/v1 Secrets are not in the grant, so the API server refuses the call. Scry can still print a secret an application logged, though: there is no redaction, and the result is stored in the control plane. A scry result is as sensitive as the logs it came from.
- /scry a service in a namespace you were not scoped to
- Out of scope, refused before a log is read. SPELLBOOK_NAMESPACES bounds what the collector can reach, and the RBAC rule bounds it again, so a namespace nobody scoped you to is unreachable even if the resolver matched a name in it.
- Diagnose it
- Scry returns log lines, not a cause. It does not correlate against a rollout, and it reads no metrics or events to build a story. Reasoning over what it finds is a separate spell; scry hands you the lines.
The audit record
A cast is a row in the database before it is anything else. The durable record is the casts row itself, so there is no separate audit table to fall out of sync with it. A cast refused at preflight never becomes a row: it is logged as a warning instead, which means a queryable casts row always describes a cast that actually ran.
cast_id cst_4a7d21c0
tenant_id tnt_northwind
principal okta|00u1a4h2 (oidc subject, not an email)
castable scry
idempotency_key slack:U03QK4T2P:1753281112
state succeeded
degraded false
started_at 2026-07-23T14:31:52Z
finished_at 2026-07-23T14:31:53Z
result { surface: slack #checkout-oncall,
target: checkout-api / storefront,
pods_read: 3, lines: 60, visibility: shared }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, so whatever log pipeline you already run picks it up with no new integration.
- Your API server's own audit log records the collector's get and list calls independently, and the cast id ties the two accounts together.
Failure modes
What happens when it cannot do the thing.
- One pod's logs fail to fetch
- The other pods still return, and the reply is marked degraded so a missing pod is visible in it. Degraded is a column on the cast, not a footnote you have to notice.
- The service is behind more than 10 pods
- Scry reads 10 and says so. It caps the fan-out rather than pulling a hundred pods' logs into one reply, and the count it read is in the result.
- A pod runs a sidecar
- Scry follows the default-container annotation when there is one and skips istio-proxy, so a multi-container pod returns the application's log rather than the mesh proxy's.
- The name matches nothing
- It lists the services it can see in scope and casts nothing. The list is private to you, because service names are not something to post to a channel by accident.
- The collector cannot reach the API server
- The cast fails with the API error, and the failure is recorded, degraded, like any other cast.
Questions
How do I read a Kubernetes service's logs without kubectl access?
Cast /scry followed by the service name, from Slack or a terminal. A collector inside your cluster resolves the name to the pods behind the service and reads the last 20 lines of each, under a ServiceAccount that holds get and list on pods and pods/log. You never hold a kubeconfig or a shell, because the read runs in the cluster under the collector's own grant, not yours.
What permissions does Scry hold?
get and list on pods and the pods/log subresource, in the namespaces the collector is scoped to. No write verb of any kind, and core/v1 Secrets are not a resource in the grant. Kubernetes RBAC enforces that ceiling at your own API server, so a bug on our side still cannot make Scry write or read a Secret.
Does Scry redact secrets out of the logs?
No. Scry returns raw log lines, and there is no redaction step. If an application logged a token, the token is in the result, and the result is stored in the control plane. A scry is exactly as sensitive as the logs behind it. Read that as a reason to fix what the application logs, not a reason to expect Scry to hide it.
Where do my logs go when I cast Scry?
The read happens in your cluster, but the result does not stay there. A cast's log lines travel back through the control plane and are stored as JSONB, isolated per tenant with row-level security and a bounded retention window. If you self-host the control plane, that store is your own infrastructure; on the managed control plane, it is ours. Either way, it is stored, unredacted.
Can an AI agent use Scry?
Yes, enrolled as its own principal with its own entitlement. An agent granted spell.scry.cast can read a service's logs and can do nothing else, because the string that would let it restart or deploy was never granted to it. The match is exact: spell.scry.cast is scry, not a family of spells.
Does Scry tell me what broke?
No. Scry hands you the log lines; it does not correlate them against a rollout, read metrics, or propose a cause. Reasoning over a service's state is a separate spell. Scry is the fast read you run first, not the diagnosis you end on.
The machine-readable version of this page is at /spells/scry.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.