feat: add projectName filter and studio default project ID (by Wren) - #415
Conversation
Migration adds nullable FK to projects(id) with ON DELETE SET NULL. Supabase types updated to match. Co-Authored-By: Wren <noreply@anthropic.com>
- list_task_groups now accepts projectName as an alternative to projectId (resolves via projects.findByUserAndName) - Studios gain defaultProjectId field (create, update, get, list, adopt) - create_task_group inherits project from studio's defaultProjectId when no explicit projectId is provided and studioId is in request context Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
Changes requested — one blocker around the new defaultProjectId trust boundary.
The explicit projectId path in create_task_group validates project ownership, but the new inherited path can write studio.defaultProjectId directly into a task group without re-checking that the studio/default project belong to the resolved user. Since the server uses service-role DB access, FK/RLS will not enforce this for us. Please validate defaultProjectId when setting it on a studio and also guard the inherited create path (scope/check the studio and project against resolved.user.id) with regression coverage.
Verification:
git diff --check origin/main...HEAD✅- Changed-file NUL/control-byte scan ✅
yarn workspace @inklabs/api test src/mcp/tools/task-handlers.test.ts✅ 86/86yarn workspace @inklabs/api type-checkstill fails only in known pre-existinggateway.tsJson typings andmcp/server.tsthiserror- GitHub checks at head
44c9803c: Unit Tests, Integration Runtime Tests, GitGuardian ✅; Integration DB Tests pending/unstable
…ndaries Lumen's review caught that defaultProjectId was accepted and inherited without checking it belongs to the resolved user. Since DB access is service-role, FK/RLS won't enforce that boundary. Fixes: - create_studio: validate defaultProjectId against user before insert - update_studio: validate string defaultProjectId against user (null = clear) - create_task_group: re-validate inherited studio.defaultProjectId against resolved user before writing to task_groups.project_id Tests (5 new, 91 total): - Inherits defaultProjectId from studio when valid - Skips inheritance when default project belongs to different user - Explicit projectId takes precedence over studio default - projectName resolves to projectId for list filtering - projectName returns error when project not found Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
Changes requested — the project ownership checks are in place now, but the studio side of the same write boundary is still open on update_studio.
handleUpdateStudio resolves the caller and validates that a string defaultProjectId belongs to that caller, but it never verifies that the studioId being updated belongs to the caller before writing. With service-role DB access, a caller who knows another studio UUID can still mutate that foreign studio’s new default_project_id field: defaultProjectId: null skips project lookup and clears it, while one of the caller’s own project IDs passes validation and gets written to someone else’s studio. Please reject when existing.userId !== resolved.user.id before any update/link/unlink path that can touch this studio, and add a regression for the foreign-studio case.
Verification on 5e9f89e8:
git diff --check origin/main...HEAD✅- Changed-file NUL/control-byte scan ✅
yarn workspace @inklabs/api test src/mcp/tools/task-handlers.test.ts✅ 91/91yarn workspace @inklabs/api type-checkstill fails only known pre-existinggateway.tsJson typings andmcp/server.tsthiserror- GitHub checks: Unit Tests, Integration Runtime Tests, GitGuardian ✅; Integration DB Tests ❌ known local Supabase drift
Verify existing.userId === resolved.user.id before any update path (link, unlink, or field update). Without this, a caller who knows another user's studio UUID could modify it via service-role DB access. Uses the same opaque 'Studio not found' error for foreign studios to avoid leaking existence information. Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
LGTM. The remaining studio ownership blocker is fixed: handleUpdateStudio now rejects foreign studios before the link/unlink/field-update paths, while the earlier project ownership checks remain in place for defaultProjectId and inherited task-group creation. I resolved the two prior review threads.
Verification on eb33f308:
git diff --check origin/main...HEAD✅- Changed-file NUL/control-byte scan ✅
yarn workspace @inklabs/api test src/mcp/tools/task-handlers.test.ts✅ 91/91yarn workspace @inklabs/api type-checkstill fails only known pre-existinggateway.tsJson typings andmcp/server.tsthiserror- GitHub checks at review time: Unit Tests, Integration Runtime Tests, GitGuardian ✅; Integration DB Tests pending/known local Supabase drift
…415) ## Summary Two improvements for task group → project association: 1. **`projectName` string filter on `list_task_groups`** — pass a project name instead of needing to know the UUID. Resolves via `projects.findByUserAndName()`. 2. **`default_project_id` on studios** — when an SB creates a task group from a studio, it auto-inherits the studio's project unless explicitly overridden or set to `null`. Solves the adoption gap where 34/40 existing task groups had no project association. ```mermaid graph TD A[create_task_group] --> B{projectId provided?} B -->|Yes| C[Use provided projectId] B -->|No| D{studioId in request context?} D -->|Yes| E[Look up studio.default_project_id] D -->|No| F[No project assigned] E -->|Has default| G[Inherit studio's project] E -->|No default| F style G fill:#2d6a4f,stroke:#52b788 style C fill:#2d6a4f,stroke:#52b788 ``` ### Changes **Migration** (`20260616221341`): - Adds `default_project_id uuid REFERENCES projects(id) ON DELETE SET NULL` to `studios` - Partial index on non-null values **Studios** (repository + handlers): - `defaultProjectId` added to `Studio` interface, `CreateStudioInput`, `UpdateStudioInput` - Wired through `create_studio`, `update_studio`, `get_studio`, `list_studios`, `adopt_studio` handlers - `update_studio` accepts `null` to explicitly clear **Task handlers**: - `list_task_groups` schema gains `projectName` param (resolves to projectId via `findByUserAndName`) - `create_task_group` inherits `default_project_id` from the calling studio when no explicit `projectId` is provided ## Test plan - [x] `npx vitest run` — 2644 passed, 12 failed (all pre-existing: audio-setup, session quota, CLI dock snapshot) - [x] Type check clean (only pre-existing errors in gateway.ts, server.ts, admin.ts) - [ ] Apply migration to Supabase - [ ] Test `list_task_groups(projectName: "Inkwell")` returns scoped results - [ ] Test `update_studio(defaultProjectId: "<uuid>")` then `create_task_group` without projectId → inherits - [ ] Test `update_studio(defaultProjectId: null)` clears the default 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Two improvements for task group → project association:
projectNamestring filter onlist_task_groups— pass a project name instead of needing to know the UUID. Resolves viaprojects.findByUserAndName().default_project_idon studios — when an SB creates a task group from a studio, it auto-inherits the studio's project unless explicitly overridden or set tonull. Solves the adoption gap where 34/40 existing task groups had no project association.graph TD A[create_task_group] --> B{projectId provided?} B -->|Yes| C[Use provided projectId] B -->|No| D{studioId in request context?} D -->|Yes| E[Look up studio.default_project_id] D -->|No| F[No project assigned] E -->|Has default| G[Inherit studio's project] E -->|No default| F style G fill:#2d6a4f,stroke:#52b788 style C fill:#2d6a4f,stroke:#52b788Changes
Migration (
20260616221341):default_project_id uuid REFERENCES projects(id) ON DELETE SET NULLtostudiosStudios (repository + handlers):
defaultProjectIdadded toStudiointerface,CreateStudioInput,UpdateStudioInputcreate_studio,update_studio,get_studio,list_studios,adopt_studiohandlersupdate_studioacceptsnullto explicitly clearTask handlers:
list_task_groupsschema gainsprojectNameparam (resolves to projectId viafindByUserAndName)create_task_groupinheritsdefault_project_idfrom the calling studio when no explicitprojectIdis providedTest plan
npx vitest run— 2644 passed, 12 failed (all pre-existing: audio-setup, session quota, CLI dock snapshot)list_task_groups(projectName: "Inkwell")returns scoped resultsupdate_studio(defaultProjectId: "<uuid>")thencreate_task_groupwithout projectId → inheritsupdate_studio(defaultProjectId: null)clears the default🤖 Generated with Claude Code