Add a second agent
Your first agent played the remediator. This page adds a detector beside it, and shows the guarantee that makes a fleet safe to grow: an agent can only touch the fields its role names, and adding one changes nothing you already wrote.
What the class already says
The incidents class gives each role its own write list:
roles:
detector: { identities: [apikey:detector], read: ["*"], write: [status, affected_service, symptoms] }
diagnoser: { identities: [apikey:diagnoser], read: ["*"], write: [hypotheses, planned_action] }
remediator: { identities: [apikey:remediator, "apikey:remediator/*"], read: ["*"], write: [status, actions_taken], claim: [remediation] }No field has two owners by accident, and no agent names another. The
starter class has all three roles. make run writes keys for two of
them, so add one for the detector:
printf 'diag-token: diagnoser\nrem-token: remediator\ndet-token: detector\n' > /tmp/keys.yaml
KSLATE_APIKEYS=/tmp/keys.yaml make runThe detector
Same two objects as before, a different credential and a different field:
from kslate import Agent, Client, Write, run
det = Client("http://127.0.0.1:8080", api_key="det-token")
def observe(fields):
return [Write(op="append", field="symptoms",
entry="checkout latency p99 8s",
evidence="grafana dashboard checkout-slo")]
run(Agent(decide=observe), det, record="incidents/inc-100")detector: appended to symptoms -> OKYou did not touch the remediator. You did not edit a graph, a router or a
list of participants. The class already said who may write symptoms,
and the detector presented a credential that maps to that role.
What it cannot do
Have the detector try to write the diagnoser’s field:
def overstep(fields):
return [Write(op="put", field="planned_action", value="restart everything",
expected_version=0, evidence="a hunch")]detector: writing planned_action -> SCOPE_DENIED (403)
{'error': 'SCOPE_DENIED', 'field': 'planned_action', 'record': 'incidents/inc-100'}The write did not land. The gateway decided this from the class and the
caller’s identity — not from anything the agent said about itself, and
not from a check in the client. A SCOPE_DENIED is a bug in your class
or your agent, not a race: retrying it will fail the same way.
Reading is scoped too
read is a separate list from write, and it applies everywhere a value
could leak: a field you may not read is simply absent from get, a
based_on naming it is refused, and a STALE rejection on a field you
may write but not read comes back with its current_value and
current_author null. Scope and
identity has the whole shape, and
the scope-test class in testdata/contexts.yaml is built to
demonstrate it.
Where the identity comes from
apikey:detector came from the token in the request, mapped by the key
file. A request cannot name its own author: there is no field for it, and
sending one is refused. On Kubernetes the same identity is
sa:<namespace>/<name>, proved by the API server — see your agent on
Kubernetes.
Next
- Handling rejections: what happens when a field your agent read changes while it is deciding.