Skip to content

fix: reject a non-string workspace id at the adapter boundary - #343

Open
ProfSynapse wants to merge 2 commits into
mainfrom
fix/workspace-id-guard
Open

fix: reject a non-string workspace id at the adapter boundary#343
ProfSynapse wants to merge 2 commits into
mainfrom
fix/workspace-id-guard

Conversation

@ProfSynapse

Copy link
Copy Markdown
Owner

Found during issue triage, not reported by anyone.

The bug

createSession accepted an object as a workspace id and created this on disk, with a shard inside it:

Nexus/data/workspaces/ws_[object Object]/

There was no validation at the adapter boundary, so anything stringifiable became a stream directory.

The fix

assertValidWorkspaceId runs before ensureInitialized() on the eight write entry points that can mint a ws_*.jsonl stream: createSession, updateSession, moveSessionToWorkspace, saveState, addTrace, createWorkspace (when an id is supplied), updateWorkspace, deleteWorkspace.

Ordering matters — before ensureInitialized(), otherwise a not-initialized error masks the missing guard and the test passes for the wrong reason.

Deliberately left alone

  • Read paths (getWorkspace, getWorkspaces, getSessions, getStates, countStates, getTraces, searchTraces) — SQLite queries only, they cannot mint a stream
  • Entity-id paths (deleteSession, updateState, deleteState) — the id they take is not a workspace id
  • The repositories themselves — the adapter is the boundary being guarded here; pushing validation deeper is a bigger change than this warrants

Tests

Ten cases, needing a real adapter instance with a ready lifecycle — the delegates are arrow-function class fields, and without a ready lifecycle ensureInitialized() throws first and hides the gap.

8 of 10 fail pre-fix; the two that pass are positive controls proving valid ids still work.

Live: object, empty string, null and a number are all rejected with the expected messages, and find /tmp/test-vault -name "*object*" returns nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C6aSoCAS5gNoew6n9qv6DJ


Generated by Claude Code

claude added 2 commits August 14, 2026 19:43
Observed during triage: passing an object as the workspace id to
`createSession` created `Nexus/data/workspaces/ws_[object Object]/` on disk,
with a shard inside it. Every workspace-scoped write derives its stream from
`workspaces/ws_${workspaceId}.jsonl`, and template interpolation stringifies
anything it is handed — so a bad id becomes a real, permanent stream whose
events belong to no workspace that exists. Nothing validated it: the adapter is
where untyped callers (tool params, plain JS, older call sites) hand data over,
and it had no check at all.

Add `assertValidWorkspaceId` and apply it to the workspace-scoped *write* entry
points, which are the ones that can create a stream:

  createSession, updateSession, moveSessionToWorkspace, saveState, addTrace,
  createWorkspace (only when an id is supplied — an omitted one is generated),
  updateWorkspace, deleteWorkspace

The read entry points were checked and deliberately left alone: getWorkspace,
getWorkspaces, getSessions, getStates, countStates, getTraces and searchTraces
only ever query SQLite by id, so a bad id returns nothing rather than writing
anything. `deleteSession`, `updateState` and `deleteState` take entity ids and
resolve the workspace from the cache, so they cannot mint a stream either.

The guard runs before `ensureInitialized()`, so a bad id fails immediately
instead of riding on a slow start. Eight of the ten new tests fail against the
pre-fix adapter (the other two are positive controls that must pass both ways).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6aSoCAS5gNoew6n9qv6DJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants