Self check
What's the problem?
The identity pipeline has one dead link.
RuntimeIdentity carries four fields. Three of them work end to end:
agent_id — filled by _identity_for(), drives per-Agent state isolation
session_id — filled by the same function, drives conversation routing
run_id — filled per delegated task/subagent, reaches skill subprocesses as
COW_AGENT_RUN_ID via apply_client_source() → bash._header_to_env
The fourth, user_id, is defined, documented ("stays None until tenancy
lands"), read by consumers — and never set by anything on the message path.
The channel layer holds the value the entire time
(ChatMessage.from_user_id / actual_user_id), but it stops at the entry
point.
What that costs today — five in-tree consumer sites that are written for
user_id and currently sit as dead code:
- Per-user data root —
state_dir.user_root() already branches to
shared_root()/users/<user_id>; the branch never fires on the chat path, so
profile/preferences/memory/output all collapse onto the Agent root.
- runs table ownership —
_RUNS_DDL carries a user_id column commented
"empty until per-user isolation lands". create_run() accepts the kwarg;
every caller leaves it "".
- Memory retrieval isolation —
memory_index_db()'s docstring states the
risk precisely: filtering must live in exactly one place because "miss one
query and it is a cross-user leak". The design is written; there is no value
to filter on.
- User-scoped run traces —
runs_dir(): "one user's runs must not be
readable by another". Same collapse.
- Per-user metering/audit on outbound calls —
apply_cloud_user() already
tags X-User-Id onto cloud API requests, but only for web-console logins
(cloud_client._console_user). IM-channel senders have no equivalent. The
framework already accepts "outbound requests carry the end user" — it just
honors it for one channel only.
Plus the third-party cost: any skill that gates an action on who asked
(deploy approval, sensitive-operation confirmation, per-user quota) has no
trustworthy source. The prompt exposes no chat_id or sender id, so anything the
model passes as an argument is self-reported and forgeable — the wrong
foundation for an authorization check.
(On my side this currently works only via a ~39-line private source patch,
which every git pull wipes.)
What would you like?
Three small, additive edits that connect user_id to the same pipe run_id
already travels — what I run locally as a patch:
-
channel/chat_channel.py — _identity_for(): fill user_id from
msg.actual_user_id or msg.from_user_id. This function already constructs
the other two fields from the same context.
_msg = context.get("msg")
_sender = ""
if _msg is not None:
_sender = (
getattr(_msg, "actual_user_id", None)
or getattr(_msg, "from_user_id", None)
or ""
)
return RuntimeIdentity(
agent_id=context.get("agent_id"),
session_id=context.get("session_id"),
user_id=(_sender or None),
)
-
common/utils.py — apply_client_source(): emit X-Agent-User-Id, exactly
parallel to the existing X-Agent-Run-Id.
-
agent/tools/bash/bash.py — map the header in header_to_env (suggested
neutral env name: COW_AGENT_USER_ID, matching the COW* family), plus a
fallback reading current_identity() directly when header propagation is
unavailable.
No behavior change for anything that doesn’t read it. The five consumer sites
above light up one by one as they opt in — none is forced to switch semantics
in this change.
Semantics worth settling in the design:
Group chats: actual_user_id is the real sender when a message was
routed on someone’s behalf → it should take priority over from_user_id.
Channels without an end user (web console before login, scheduler boot):
leave None, do not substitute a session id — consumers must be able to tell
“unset” apart from “this channel has no user”.
Whether create_run(user_id=...) starts writing from the ambient identity is
a natural follow-up; this change only makes the value available.
Happy to send a PR for the three edits if the direction is acceptable.
Environment: CowAgent 2.1.9 (7236846), Linux, Feishu channel.
Contribution
Self check
What's the problem?
The identity pipeline has one dead link.
RuntimeIdentitycarries four fields. Three of them work end to end:agent_id— filled by_identity_for(), drives per-Agent state isolationsession_id— filled by the same function, drives conversation routingrun_id— filled per delegated task/subagent, reaches skill subprocesses asCOW_AGENT_RUN_IDviaapply_client_source()→bash._header_to_envThe fourth,
user_id, is defined, documented ("stays None until tenancylands"), read by consumers — and never set by anything on the message path.
The channel layer holds the value the entire time
(
ChatMessage.from_user_id/actual_user_id), but it stops at the entrypoint.
What that costs today — five in-tree consumer sites that are written for
user_idand currently sit as dead code:state_dir.user_root()already branches toshared_root()/users/<user_id>; the branch never fires on the chat path, soprofile/preferences/memory/output all collapse onto the Agent root.
_RUNS_DDLcarries auser_idcolumn commented"empty until per-user isolation lands".
create_run()accepts the kwarg;every caller leaves it
"".memory_index_db()'s docstring states therisk precisely: filtering must live in exactly one place because "miss one
query and it is a cross-user leak". The design is written; there is no value
to filter on.
runs_dir(): "one user's runs must not bereadable by another". Same collapse.
apply_cloud_user()alreadytags
X-User-Idonto cloud API requests, but only for web-console logins(
cloud_client._console_user). IM-channel senders have no equivalent. Theframework already accepts "outbound requests carry the end user" — it just
honors it for one channel only.
Plus the third-party cost: any skill that gates an action on who asked
(deploy approval, sensitive-operation confirmation, per-user quota) has no
trustworthy source. The prompt exposes no chat_id or sender id, so anything the
model passes as an argument is self-reported and forgeable — the wrong
foundation for an authorization check.
(On my side this currently works only via a ~39-line private source patch,
which every
git pullwipes.)What would you like?
Three small, additive edits that connect
user_idto the same piperun_idalready travels — what I run locally as a patch:
channel/chat_channel.py—_identity_for(): filluser_idfrommsg.actual_user_id or msg.from_user_id. This function already constructsthe other two fields from the same context.
common/utils.py — apply_client_source(): emit X-Agent-User-Id, exactly
parallel to the existing X-Agent-Run-Id.
agent/tools/bash/bash.py — map the header in header_to_env (suggested
neutral env name: COW_AGENT_USER_ID, matching the COW* family), plus a
fallback reading current_identity() directly when header propagation is
unavailable.
No behavior change for anything that doesn’t read it. The five consumer sites
above light up one by one as they opt in — none is forced to switch semantics
in this change.
Semantics worth settling in the design:
Group chats: actual_user_id is the real sender when a message was
routed on someone’s behalf → it should take priority over from_user_id.
Channels without an end user (web console before login, scheduler boot):
leave None, do not substitute a session id — consumers must be able to tell
“unset” apart from “this channel has no user”.
Whether create_run(user_id=...) starts writing from the ambient identity is
a natural follow-up; this change only makes the value available.
Happy to send a PR for the three edits if the direction is acceptable.
Environment: CowAgent 2.1.9 (7236846), Linux, Feishu channel.
Contribution