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.
Narrowed out of
projects/eve/issues/per-caller-identity.mdat synthesis 1. Thatdraft 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: threadkeys history by conversation key (a channel, a thread,a webhook caller's
conversation_id), and that key is the only identity in thesystem. 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 thatchannel 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 runsrecords that arun 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.currentis the caller of this turn,session.auth.initiatoriswhoever created the session. A person resuming a schedule-started session becomes
currentwhileinitiatorstays the app. Automated turns carry a syntheticprincipal (
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
AuthFnaccepting a request means a 401, and body-supplied identity is nevertrusted, only a verified signature or token.
eliza (
elizaOS/eliza@b8ac8ded, MIT) supplies the identity model. AnEntityis a person withnames: string[]and oneComponentper platformholding that platform's
sourceEntityId. AWorld(server or workspace) carriesWorldOwnership { ownerId }and aRoleper member:OWNER,ADMIN,MEMBER,GUEST,NONE, commented in their source as being for access control. TheirChannelTypenormalizes conversations across platforms (DM,GROUP,THREAD,VOICE_GROUP,AUTONOMOUS, ...), so a rule like "only respond to mentions in agroup" 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 Telegramuser, 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. Cronand 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.Semantics kept to the same shape as
permissions:: omit the block and behaviouris 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:
callers:should eventually default to deny. Consistency withpermissions:argues yes; not breaking existing agents argues for adeprecation path rather than a flag day. My inclination is to ship permissive,
warn in
af validatewhen a trigger is public-facing and unlisted, and revisit.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).
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.
OWNER/MEMBER/GUESTis moreexpressive 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.
eliza's
Entity.names: string[]records that a merge happened without recordingwho 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 runsand theaudit 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.