Skip to content

Semantic set_id derivation is ambiguous: distinct (org, project, metadata) scopes map to the same set_id #1716

Description

@edwinyyyu

Where

SemanticSessionManager._generate_set_id (packages/server/src/memmachine_server/semantic_memory/semantic_session_manager.py:430), with _org_set_id (:415) and _hash_tag_list (:39). Observed on main at 8101f14.

The id is built as

mem_{set_type}_org_{org_id}[_project_{project_id}]_{len(metadata)}_{shake256(sorted keys)[:12]}__{'_'.join(sorted(f"{k}_{v}"))}

Every component is joined with _ and nothing is escaped. SafeId permits _ in org_id and project_id, metadata values are str(value) with no validation, and the hash covers only the metadata keys, not the values or the scope. So two distinct (org_id, project_id, metadata) scopes can produce the same string.

Measured collisions

Run against the real generator on main:

COLLIDE  org "a_project_b", org-level set, {producer_id: x}   vs   org "a", project "b", {producer_id: x}
    mem_user_set_org_a_project_b_1_7e8d0b20c039__producer_id_x

COLLIDE  same org/project, {a: "x_b_y", b: "z"}   vs   {a: "x", b: "y_b_z"}
    mem_other_set_org_o_project_p_2_b226bacd172e__a_x_b_y_b_z

COLLIDE  org "victim", project "prod", {team: eng}   vs   org "victim_project_prod", org-level, {team: eng}
    mem_other_set_org_victim_project_prod_1_05ab79662e20__team_eng

distinct org "a_project_b", org-level, {}   vs   org "a", project "b", {}
    (set_type vs project_set prefix differs)

The three default sets (org-level with no tags, org-level user set, project-level with no tags) do not collide with each other across these scopes. The first and third cases need a project-scoped custom set type with at least one tag on one side; the second needs a set type with two or more tags.

Consequence

The set id is the partition key for features, ingestion history, cluster state, and per-set configuration. Two scopes that share an id read each other's features in search, ingestion consolidates their facts together, deleting the set removes both, and register_set_id_set_type (config_store_sqlalchemy.py:357) is ON CONFLICT DO NOTHING on set_id, so whichever scope registers first owns the set-type mapping.

Any deployment that scopes callers by org_id and lets callers choose org or project names is exposed to cross-scope access through a chosen name; a deployment that assigns opaque ids is not.

Related dead code

_get_all_set_ids (:303) lists sets with list_set_id_starts_with(prefix="org_..."), but every set id starts with mem_, so it can never match. It also has no callers.

Fix direction

Derive the id from a canonical encoding of the whole tuple, for example a hash of the JSON of [org_id, project_id, sorted(metadata.items())], or escape the separator in every component. Either changes every existing id, so it needs a migration in the style of alembic_pg/versions/b65f7f4a9d2c_migrate_legacy_set_ids.py. Remove _get_all_set_ids or make it match the real prefix.

🤖 Written by Claude Fable 5.1 via Claude Code; posted from @edwinyyyu's account.

Activity

  1. added theissue type on Sep 28, 2026
  2. added
    securitySecurity-related tasks that come from private reports, code scanning, and vulnerability checks.
    multiuserMulti-user support: isolation and behavior across users, orgs and projects
    on Sep 29, 2026
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

    multiuserMulti-user support: isolation and behavior across users, orgs and projectssecuritySecurity-related tasks that come from private reports, code scanning, and vulnerability checks.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions