Skip to content

Make TicketTask a first-class workspace-scoped aggregate #1673

Description

@seonghobae

Problem

Current TicketTask persistence has user_id and organization_id but no workspace_id. PR #1486 can scope email-linked task joins through Email.workspace_id, but the task aggregate itself cannot enforce workspace ownership. CodeRabbit's exact-head review identified the remaining WebDAV path: a task id owned by the same user/organization can still be addressed from another workspace because webdav_service has no task-level workspace predicate; hiding the joined email is not an ownership invariant.

This is a structural authorization/data-model gap, not a route-specific query tweak. Fix it in the Naruon canonical owner rather than adding another ACL workaround.

Required domain/schema repair

  • Add non-null workspace_id to TicketTask as part of the aggregate owner scope (user_id, organization_id, workspace_id).
  • Add an Alembic migration that backfills email-linked tasks from email_records.workspace_id and defines an explicit, deterministic policy for existing source-less tasks; no guessed cross-workspace default.
  • Move task repositories/queries/mutations (API, WebDAV, Reply-SLA, imports/workers) to the same three-part workspace owner invariant.
  • Preserve external/legacy compatibility at boundaries; do not duplicate source ownership truth in consumers.
  • Add cross-workspace negative tests for GET/list/update/WebDAV/Reply-SLA paths and a real PostgreSQL migration acceptance test.
  • Update DDD/ADR/data-model documentation so TicketTask workspace ownership is authoritative rather than inferred from an optional related email.

Acceptance

  1. A same-user/same-organization session in workspace B cannot discover, read, mutate, sync, or infer the existence/state of a task owned by workspace A, whether the task has a related email or is source-less.
  2. Existing migrated rows receive an auditable workspace assignment or fail migration/provisioning closed; no silent arbitrary assignment.
  3. Repository-owned tests plus real PostgreSQL migration tests are GREEN on the unchanged exact head.
  4. PR feat(noema-agent): add calendar conflict-check tool #1486's WebDAV review finding is resolved by the aggregate invariant, not by route-local masking.

Evidence: PR #1486 review thread PRRT_kwDOSNjZ2s6hzF2d, predecessor exact head 9ebb5953463059eaae3a5053ef4623ef89347203.

Live validation — 2026-09-19

Exact source audit on #1486 22c173bb93ef15c3244477fe32540aeb2bcf1199 confirms this is still a real aggregate-boundary RED, not a stale review finding:

  • backend/db/models.py::TicketTask still has user_id and nullable organization_id but no workspace_id.
  • backend/api/tasks.py::_build_task_query scopes joined Email rows by workspace but explicitly admits TicketTask.related_email_id IS NULL; source-less tasks therefore have no workspace ownership predicate for list/PATCH.
  • backend/services/webdav_service.py::determine_knowledge_materialization_intent_from_db constrains the optional joined Email by workspace but selects TicketTask itself only by task/user/organization.
  • backend/services/noema_agent.py::tool_list_tasks and tool_update_task_status select/update tasks by user/organization (plus task uid for update) without workspace, even though the agent dependency already carries workspace_id.

The implementation order remains constrained by migration ownership. #1503 9151c75568c582c8147cfee6757cd00a9b4d60b7 owns the deterministic workspace owner/organization bindings and canonical 0018_workspace_registry → 0019_email_read_state_repair → 0020_workspace_organization_binding → 0021_workspace_personal_owner_binding chain. #1486 carries a separate descendant migration history. Adding TicketTask.workspace_id directly on the unreconciled #1486 ancestry would either guess a source-less workspace or cement a parallel/colliding migration lineage.

Decision: keep this issue open/high and do not mask it with another route predicate. First reconcile the canonical workspace/migration owner through the normal non-force stack, then implement the aggregate invariant. After reconciliation, email-linked tasks backfill from Email.workspace_id; a source-less task may be assigned only from one auditable canonical workspace-owner binding. Zero or multiple valid candidates must fail migration/provisioning closed with explicit diagnostics; no arbitrary personal/default workspace is allowed.

Required negative acceptance after that reconciliation includes Tasks API source-less list/PATCH, WebDAV task lookup/materialization, Noema list/update, Reply-SLA creation/reload, imports/workers, and a real PostgreSQL migration containing ambiguous and unassignable legacy rows.

Live prerequisite topology

The earlier shorthand .github#2040 repair → external dispatch is no longer sufficient. Protected .github/main remains 64aa08d7fa487deacd41c761c36277ca68cab6c9; scheduler/producer owner #2040 remains exact 12c3fa6f3623aa5f2979d3d5ee4ed987002a6c0d, Draft/open/non-mergeable. Because both sides changed the shared scheduler after merge base fb17ef556f94f673234aa557254ae52779e9a7b0, #2040 must first perform ordinary/non-force path-wise protected-main reconciliation, preserving its valid owner deltas while adopting compatible current-main queue/coalescing/capacity behavior and resolving the production repository-identity RED.

Central base-SAST/security is also a distinct prerequisite. .github#2269@a83d6d325c28620bb393d7b79a3cc74fe8130c38 and .github#2272@5b9e8642361818769d58af6f4e17a6087c90f6ad both harden initial https://api.github.com admission, but default urllib.request redirect handling still has a reproduced credential-egress RED: a cross-origin 30x can carry the Authorization header unless redirects are rejected or every redirect target is revalidated. #2272 additionally carries the deploy-pages.yml caller-input shell-injection repair, but central Semgrep excludes .github/workflows, so executable regression coverage is still required. #2272 has not yet fully inherited #2269's focused CodeQL-helper regression delta; sibling closure/supersession is therefore not justified.

Related #2268/#2271 repository-identity slices are source authority only until their own acceptance settles. The external authenticated repository_dispatch canary remains independent of repository-local CodeQL success.

Current dependency order is therefore: central #2269/#2272 valid production+test delta convergence or a verified complete successor → #2040 path-wise protected-main reconciliation with #2268/#2271 adopt/adapt where applicable → fresh exact-head central security/SAST/CodeQL + qualifying review → unchanged external dispatch canary → #1623 revalidation against the then-current vulnerability database and protected integration → #1694#1691#1503 protected workspace/migration owner → #1486 ordinary adoption/rechain → this #1673 aggregate implementation and exact-head PostgreSQL/security/review acceptance.

This ordering is an owner/migration repair finding, not permission to close or silently defer the product gap.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions