Skip to content

[Feature] wire up RuntimeIdentity.user_id — 5 consumer sites are already waiting for it #3202

Description

@592774447

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:

  1. 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.
  2. 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 "".
  3. 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.
  4. User-scoped run traces — runs_dir(): "one user's runs must not be
    readable by another". Same collapse.
  5. 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:

  1. 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),
    )
  2. common/utils.py — apply_client_source(): emit X-Agent-User-Id, exactly
    parallel to the existing X-Agent-Run-Id.

  3. 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

  • I'd be interested in helping implement this.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions