Repository navigation
Description
Summary
In a project bound to a freshly created, empty workspace, the model's datamate_search_memory tool call returns memories that belong to other workspaces and other projects on the same machine, and the model presents them as what "the team" has on record for this project. The engine's memory hub is not scoped to the bound workspace.
Found on 0.12.1 (tenant anandtest1) while testing the v0.12.0/v0.12.1 link flows end to end. This is the read side of #1332 (writes go to the engine hub rather than the workspace mirror); it is a different mechanism from #1140 (server-side list scoping), because the content comes back through a live tool call, and it includes scope: project blocks, not only global ones.
Reproduction (≤ 6 steps)
- Workspace 1 (
orders-analytics) has a project-scoped memory block written by an earlier session:orders-analytics/.altimate-code/memory/reporting/1997-europe-revenue-reconciliation.md("Team-standard answer: 6,612,330,499.498 (~6.6B) is authoritative for 1997 EUROPE revenue"). - Create an unrelated throwaway project (
~/demos/workspace-pilot/wtest/link-1, fake remote) and link it withaltimate-code link→ "Create a quick workspace" → workspace 6, which has no memory and no skills. The project has no.altimate-code/memorydirectory. - In
link-1:ALTIMATE_WORKSPACE=1 altimate-code run --yolo --model openai/gpt-5.6-sol "What did the team decide about the 1997 EUROPE revenue number? Just tell me what is on record." - The model calls
datamate_search_memorywith{"query": "team decision on the 1997 EUROPE revenue number; what is on record"}and answers:
The team recorded 1997 EUROPE revenue as 6,612,330,499.498 (~6.6B) in net revenue—after discounts, before tax. This is the authoritative figure from TPCH_ANALYTICS.PUBLIC_REPORTING.RPT_MONTHLY_SALES_BY_REGION.
The session export (altimate-code export ses_f38e41717fferS2jWTnM6DY7XO) shows the tool output, which is a list of memories from at least two other contexts:
"Team standard requires all revenue reporting to use net revenue (after discounts) rather than gross revenue. …" ← workspace 1, orders-analytics
"When working on revenue-related SQL issues, follow this workflow: (1) Use the board-revenue-review tool to review revenue.sql against the finance contract requirements, (2) Run python3 check_revenue.py …" ← a different project entirely
"The certified table TPCH_ANALYTICS.PUBLIC_REPORTING.RPT_MONTHLY_SALES_BY_REGION serves as the authoritative source of truth … For 1997, this table shows total NET_REVENUE of approximately 33.0B (33,049,396,500.2864) …" ← workspace 1
Logs for the run (OPENCODE_PRINT_LOGS=1): adopted the workspace binding this project already has on the server datamateId=6, then datamate_search_memory allowed and executed. Full log: ~/demos/workspace-pilot/usecases/leak-repro.log. The same happened in the first observation (a plain revenue question in the same project with --integrations local): the answer cited "Europe's confirmed amount is $6,612,330,499.50" and "The certified 1997 total is $33,049,396,500.29", both figures that exist only in workspace 1's memory.
Why it matters
- Workspace isolation is the contract feat(workspace): state the linked workspace every turn, and mean the Altimate one #1330 states to the model every turn ("this workspace and never another"). Here a brand-new workspace inherits another workspace's decisions on its first question, and the model attributes them to "the team".
- For a consultant or a platform team linked to several customers' or departments' workspaces from one machine, this is a cross-tenant-style leak within one account: content written for workspace A is served in workspace B.
- The local mirror index (
~/.local/state/altimate-code/altimate-workspace-memory-index.json) keys records per workspace, andmemory-sync.tsfilters hydration withbelongsHere(), so the harness's own path is scoped. The tool path is not:datamate_search_memoryis served by the engine, whose memory hub appears to be per user/machine rather than per--datamate <id>.
Suggested fix
Either scope the engine's memory tools to the workspace the engine was started for (pass the datamate id into the hub's search and add calls, and partition storage by it), or route datamate_search_memory / datamate_add_memories through the harness's workspace memory layer, which already knows the binding, so the engine hub never answers for a workspace it was not started for. Until then, disable the engine memory tools when a workspace is bound and let altimate_memory_read serve searches from the scoped mirror.
Activity
anandgupta42 commentedon Sep 22, 2026
@sahrizvi this is the one to look at first from tonight's workspace release tests. I reproduced it myself after the tester reported it, with a plain question and no hints; the session export in the body shows the tool output verbatim. It ties to #1332: writes go to the engine hub, and this is what the hub gives back to any workspace on the machine. The other #1318 flows (quick workspace on a linked project, rebind to an existing workspace, path-keyed link with no remote) all passed, with recordings under
~/demos/workspace-pilot/recordings/wc*.saravmajestic commentedon Sep 23, 2026
Confirmed and fixed, but the mechanism is not quite the one in Suggested fix — worth recording because the first option there would not have closed it.
Two causes, not one
1. The engine asked for every workspace.
ToolBuilder.getSharedDatamateIdsreturned every public or restricted workspace the user could see, not the bound one, and passed them all asdatamate_id.2. The lambda's query for the caller's own records has no workspace dimension.
/memory/searchand/memory/listrun two queries and union them:datamate_idonly feeds the second query. The memories that leaked here were written by the same user, so the first query returned them regardless of what the engine sent — which means "pass the datamate id into the hub's search" alone would not have fixed the reproduction. A private "quick workspace" was in fact the worst-scoped case: the engine sent no filter at all for it.What changed
AltimateAI/mcp-memory-lambda#17— a newscope_datamate_idconfines the caller's-own query to the bound workspace, with a second server-side filtered query for the records stamped for it, so ranking stays correct.AltimateAI/altimate-mcp-engine#253— narrowsdatamate_idto the bound workspace and sendsscope_datamate_id(including for a private workspace, whichdatamate_idmust never carry since it makes the lambda queryuser_id: "*").Either can ship first; an older client and an older lambda both degrade to today's behaviour rather than breaking.
Two decisions worth flagging
This does not revert AI-8908. That PR's decision is "a grantee reads memories written by other members of that workspace" — singular. The all-workspaces list predates it; AI-8908 renamed
getPublicDatamateIds, which already returned all of them.Untagged records are kept. A record with no
metadata.datamate_idbelongs to the account rather than a workspace — including altimate-code's global-scope blocks, which carry a workspace only when the block is project-scoped. They stay readable in every workspace. Note this does not re-open the leak through altimate-code's own writes: those carrysource: altimate-code, which the backend already strips from/datamates/memory/searchand/listunless the caller opts in, and the engine never does.Not yet verified end to end against a deployed lambda — the exact reproduction in this issue is the right post-deploy check.
saravmajestic commentedon Sep 23, 2026
Update — the fix above was reworked into something simpler, so the
scope_datamate_idparameter I described is gone.The framing that landed: a read for a workspace returns what belongs to that workspace, not to the caller. The user-scoped query (
user_idANDapp_id) is not a workspace query at all — it has no workspace dimension and always ran. So when a workspace is named it is now replaced rather than unioned with.A brand-new workspace therefore asks only for its own memory and gets nothing back, which is the answer this issue expected. Nothing new goes on the wire:
datamate_idalready names the workspace.Two consequences worth recording:
datamate_idno longer appear in a workspace read. They belong to no workspace. Nothing is deleted — it is a read filter, and they are still returned to a caller that names no workspace, such as the tenant-wide memory hub.With no workspace named, every query is unchanged, so the two repos can ship in either order and non-workspace callers are untouched.
Still to confirm after deploy: the exact reproduction in this issue.
anandgupta42 commentedon Nov 5, 2026
Consolidated into #1678, which tracks this together with related issues. The original report here stays linked from that tracker.