Skip to content

runtime: record who sent an event, and let the agent file say whose events it accepts #175

Description

@RatulMaharaj

Narrowed out of projects/eve/issues/per-caller-identity.md at synthesis 1. That
draft bundled two features: knowing who the caller is, and resolving credentials
per caller. The second depends on
#172 for any
interactive OAuth flow and on a web surface we do not have. The first stands
alone, is a security gap today, and is a prerequisite for the second. This issue
is only the first.

Problem

An agent knows which conversation an event belongs to and nothing about who sent
it. memory.scope: thread keys history by conversation key (a channel, a thread,
a webhook caller's conversation_id), and that key is the only identity in the
system. Two consequences:

An agent in a shared channel serves everyone equally. Put an agent with
permissions.run: [gh] in a Discord channel and anyone who can type in that
channel can make it act. The permission model bounds what the agent can do,
which is the hard half, and says nothing about who may ask. For a bot in a
private team channel that is fine. For anything in a public or semi-public space
it means the blast radius the agent file describes is available to strangers.

The audit trail cannot answer "who asked for this". af runs records that a
run happened, what it did, and every permission decision. It cannot say which
person caused it. For a framework whose pitch includes an audit trail for
unattended operation, the actor is a strange thing to be missing, and it is the
first question anyone asks after an incident.

This also blocks pillar 3. A meeting agent that cannot tell participants apart
cannot act on behalf of the person who asked, and "the meeting on the 14th, where
Sam asked me to file the issue" needs a Sam.

Evidence

eve (vercel/eve @ 7ec1c93, Apache-2.0) puts two fields on every session:
session.auth.current is the caller of this turn, session.auth.initiator is
whoever created the session. A person resuming a schedule-started session becomes
current while initiator stays the app. Automated turns carry a synthetic
principal (authenticator: "app", principalId: "eve:app",
principalType: "runtime"), so policy can distinguish "the cron fired this" from
"a human asked for this" without special-casing. Their route auth fails closed:
no AuthFn accepting a request means a 401, and body-supplied identity is never
trusted, only a verified signature or token.

eliza (elizaOS/eliza @ b8ac8ded, MIT) supplies the identity model. An
Entity is a person with names: string[] and one Component per platform
holding that platform's sourceEntityId. A World (server or workspace) carries
WorldOwnership { ownerId } and a Role per member: OWNER, ADMIN, MEMBER,
GUEST, NONE, commented in their source as being for access control. Their
ChannelType normalizes conversations across platforms (DM, GROUP, THREAD,
VOICE_GROUP, AUTONOMOUS, ...), so a rule like "only respond to mentions in a
group" is written once instead of per connector.

Detail: projects/eve/artifacts/durable-execution-and-parking.md,
projects/eliza/artifacts/identity-model.md.

What a Looped AF version looks like

Two pieces. The first has no config surface and should land first.

1. A principal on every run. Every trigger already knows who sent the event: a
Discord user id and display name, a Slack member id, an email From, a Telegram
user, the authenticated identity of a webhook caller, or the scheduler itself.
Record it on the run, show it in af runs, and put it in the audit trail. Cron
and schedule-fired runs get a synthetic principal so the field is never blank and
"nobody" is never confused with "not recorded". The model sees nothing new by
default.

This is useful immediately and independently. It is also the smallest change that
makes the audit trail answer the question people actually ask it.

2. callers:, an allowlist of who may wake the agent.

triggers:
  - type: discord
    channels: ["issues"]

callers:
  discord: ["role:engineering", "user:ratul"]
  email: ["*@example.com"]
  webhook: any          # already gated by the webhook secret

Semantics kept to the same shape as permissions:: omit the block and behaviour
is unchanged (anyone the trigger already lets through), because tightening this by
default would break every deployed agent. Present, it is deny-by-default per
listed trigger type. A refused event never reaches the model, gets the same short
built-in reply a full queue gets, and lands in the audit trail as a refusal.

Decisions to settle:

  • Whether callers: should eventually default to deny. Consistency with
    permissions: argues yes; not breaking existing agents argues for a
    deprecation path rather than a flag day. My inclination is to ship permissive,
    warn in af validate when a trigger is public-facing and unlisted, and revisit.
  • What identity a webhook caller has. Today the shared secret authenticates
    the caller system, not a person. Either that is the principal (honest, coarse)
    or the payload can carry one (and then it must be signed, per eve's rule that
    body-supplied identity is never trusted).
  • How much identity the model sees. It should be able to know who is talking
    to it, since an agent that cannot address people by name is worse at its job. It
    should never see credentials. Those are separate, and the split is the security
    property.
  • Roles, or just identities? eliza's OWNER/MEMBER/GUEST is more
    expressive than a list of ids and needs a source of truth for role membership,
    which on Discord is a guild role and on email is nothing. Starting with platform
    identities and platform-native groups (a Discord role, a Slack usergroup) avoids
    inventing a role system.
  • Explicitly not in scope: linking one person's identities across platforms.
    eliza's Entity.names: string[] records that a merge happened without recording
    who authorized it, and for an agent that acts on someone's behalf "these two
    accounts are the same human" is a claim that needs provenance. If we ever do it,
    it should be asserted by an operator, never inferred from a matching name.

Acceptance criteria

Every run records the principal that caused it and shows it in af runs and the
audit trail, with a synthetic principal for scheduled runs. An agent file can
declare which callers a trigger accepts, and a refused event is recorded without
reaching the model.

Metadata

Metadata

Assignees

No one assigned

    Labels

    researchFindings from the agent-framework competitive research loop

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions