fix(runtime): record company_id on FUSE workspace and CLI agent state - #207
Merged
Merged
Conversation
- Persist authenticated company_id in PUT /runtime/fs/write and update on conflict - Populate company_id in CLI tasks add, memory note, log note, and climate updates - Add defense-in-depth per-agent cleanup when deleting workspaces to prevent orphan data - Verify with devtools visibility and cascade deletion integration tests
This was referenced Sep 5, 2026
Merged
WhichPaths
added a commit
to WhichPaths/cumora
that referenced
this pull request
Sep 5, 2026
Rebased onto main and reduced to what yetone#207 did not cover, per review. Dropped: the fs-endpoints.ts change. yetone#207 landed it, and its version is better — the tenant comes from c.companyId (the JWT claim the endpoints already 403 on when empty) rather than a subquery against participants, so it is one round trip fewer and cannot write NULL when the participant row is missing. The test that exercised that endpoint went with it; yetone#207 asserts the same property in runtime-server.test.ts. Kept, and the only behaviour change here: agent_events, agent_runs and agent_triages join the by-owner sweep, taking it from five tables to eight. Worth being precise about why, because it is not the same reason as the five. Those five were leaking: their writers were dropping company_id, so the tenant sweep could not see the rows. These three have writers that all pass a tenant today — I checked every createRun, recordEvent and recordTriage call site — so nothing is leaking through them right now. They are here so the sweep stops depending on writer discipline at all, which is the property that failed for the other five. And they are the tables carrying per-run history and cost, so a row that does slip through resurfaces on a billing report rather than in a UI. Six of the seven integration tests pass against main's five-table list; the seventh is the one that pins the three, and it seeds them the way a writer that forgot the tenant would. Two are guards in the other direction: a correctly-tenanted row is still removed, and another workspace keeps its own run history.
yetone
pushed a commit
that referenced
this pull request
Sep 6, 2026
Rebased onto main and reduced to what #207 did not cover, per review. Dropped: the fs-endpoints.ts change. #207 landed it, and its version is better — the tenant comes from c.companyId (the JWT claim the endpoints already 403 on when empty) rather than a subquery against participants, so it is one round trip fewer and cannot write NULL when the participant row is missing. The test that exercised that endpoint went with it; #207 asserts the same property in runtime-server.test.ts. Kept, and the only behaviour change here: agent_events, agent_runs and agent_triages join the by-owner sweep, taking it from five tables to eight. Worth being precise about why, because it is not the same reason as the five. Those five were leaking: their writers were dropping company_id, so the tenant sweep could not see the rows. These three have writers that all pass a tenant today — I checked every createRun, recordEvent and recordTriage call site — so nothing is leaking through them right now. They are here so the sweep stops depending on writer discipline at all, which is the property that failed for the other five. And they are the tables carrying per-run history and cost, so a row that does slip through resurfaces on a billing report rather than in a UI. Six of the seven integration tests pass against main's five-table list; the seventh is the one that pins the three, and it seeds them the way a writer that forgot the tenant would. Two are guards in the other direction: a correctly-tenanted row is still removed, and another workspace keeps its own run history. Co-authored-by: Xialie Zhuang <62231346+Lieisyourlie@users.noreply.github.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem & Real-World Impact
Cumora enforces per-tenant multi-tenant isolation where per-agent data (
agent_workspace,agent_climate,agent_tasks,agent_log) must be scoped tocompany_id(as added in schema migrations 583-636, 718, 835, 1885 and documented inskills.ts:250-252andcli.ts:55-58).However, several write paths omitted
company_idwhen persisting records:server/src/agents/runtime/fs-endpoints.ts(PUT /runtime/fs/write):When agents write files to
/workspacevia the FUSE virtual filesystem driver in their pod,c.companyIdwas available in the JWT claims but omitted from theINSERT INTO agent_workspacestatement, leavingcompany_idasNULL.GET /devtools/agent-workspace/treeand/file(router.ts:5357,5377) queryWHERE agent_id = $1 AND company_id = $2. Becausecompany_idwas NULL, files written by agents via FUSE were completely invisible in the UI (returning[]or404).DELETE /api/companies/:id(router.ts:1827) cleans up soft-scoped tables usingDELETE FROM ${table} WHERE company_id = $1. Becausecompany_idwas NULL, files created via FUSE were never deleted upon workspace deletion, permanently leaving orphaned agent workspace data in Postgres after the owner and participants were removed.cascadeAgentIdChange),agent_workspaceis updated withWHERE company_id = $3(migrate.ts:1885). NULLcompany_idrows failed to match, severing file ownership upon rename.server/src/agents/cli.ts&climate.ts:tasks add: retrievedcompanyId = await agentCompany(me)on line 4448 and emitted it in telemetry, but omittedcompany_idinINSERT INTO agent_tasks.climate note&bumpClimate: omittedcompany_idinINSERT INTO agent_climate, causing records to default to'personal'and bypass workspace deletion.memory note&log note: omittedcompany_idinINSERT INTO agent_log.Solution
company_id: c.companyIdinINSERT INTO agent_workspaceinserver/src/agents/runtime/fs-endpoints.ts.company_id = COALESCE(EXCLUDED.company_id, agent_workspace.company_id)on conflict.company_idinagent_tasks,agent_climate, andagent_log.company_idinbumpClimateinserver/src/agents/climate.ts.DELETE /api/companies/:id(server/src/api/router.ts), explicitly purge any per-agent rows for the company'sagentIdsacross['agent_workspace', 'agent_memory', 'agent_log', 'agent_tasks', 'agent_climate']to guarantee zero orphaned data even for pre-existing or legacy rows.Verification
company_idpreviously storednull, now stores the authenticatedcompanyId.server/src/__integration__/runtime-server.test.tsto assertagent_workspace.company_idmatches the agent's tenant (34/34 pass).server/src/__integration__/workspace-management.test.tsto seedagent_workspace,agent_tasks,agent_climate, andagent_log, delete the workspace, and assert all rows are completely purged from Postgres (12/12 pass).npm test: 1127 passed, 4 skipped, 0 failed.npm run test:integration: 297 passed, 5 skipped, 0 failed.npm run lint: 485 files checked, 0 errors.npm run typecheck&npm run server:typecheck: 0 errors.npm run guard:big-brain,guard:llm-tracked,guard:engine-registry: all passed.