Skip to content
Add a second agent

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 run

The 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  -> OK

You 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