openapi: 3.1.0
info:
  title: Workspace (Projects, Scenarios & Sharing)
  version: 1.0.0
  description: |
    MA-55 — v1 of the Workspace feature: Projects (named containers for Scenarios), Scenarios (the atomic unit of analytical work, including sandbox / project-less scenarios), and org-level sharing. Every operation here is a control-plane / management-plane operation: 0 credit cost and no `meta.provenance` (spec §8's explicit carve-out — ADR-7/TF-93 provenance applies to data-product endpoints only). Auth (revised 2026-08-19): Projects/Scenarios operations accept EITHER a JWT OR an X-API-Key (never both — `400 AMBIGUOUS_AUTH`); a JWT-created resource is owned by that user, an X-API-Key-created resource is owned by the key's org. Org-management operations (`listWorkspaceOrgs`, `activateWorkspaceOrg`, `listWorkspaceSharingOrgs`) stay JWT-only. See `docs/products/workspace-projects-scenarios.md` § "Auth mode per endpoint" for the full per-operation table and `docs/architecture/0036-tag-based-resource-permissions.md` for the underlying tag-based ownership/sharing model (MA-64).
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com
servers:
  - url: https://api.mworks.com/v2
    description: Production
components:
  securitySchemes:
    apiKey:
      type: apiKey
      name: X-API-Key
      in: header
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    OrgSummary:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    Project:
      type: object
      required:
        - id
        - name
        - description
        - created_at
        - updated_at
        - archived_at
        - access
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        archived_at:
          type: string
          format: date-time
          nullable: true
        access:
          type: string
          enum:
            - owner
            - recipient
    CreateProjectRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 2000
    UpdateProjectRequest:
      type: object
      description: At least one of `name`/`description` must be provided.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 2000
          nullable: true
    ProjectSharing:
      type: object
      required:
        - shared_org_ids
        - shared_by_user_id
        - shared_at
      properties:
        shared_org_ids:
          type: array
          items:
            type: string
            format: uuid
        shared_by_user_id:
          type: string
          format: uuid
          nullable: true
        shared_at:
          type: string
          format: date-time
          nullable: true
    UpdateSharingRequest:
      type: object
      required:
        - shared_org_ids
      description: PUT semantics — replaces the full shared-org list. Empty array = fully private.
      properties:
        shared_org_ids:
          type: array
          items:
            type: string
            format: uuid
    ScenarioType:
      type: string
      description: |
        'marketplace' is deliberately excluded from v1 — no worker or OpenAPI spec exists yet for that product (spec §2.5).
      enum:
        - inventory_plan
        - place_select
        - set_dynamics
    Scenario:
      type: object
      required:
        - id
        - project_id
        - name
        - type
        - description
        - payload
        - is_sharing_private
        - created_at
        - updated_at
        - archived_at
        - access
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
          nullable: true
          description: null = sandbox scenario
        name:
          type: string
        type:
          $ref: '#/components/schemas/ScenarioType'
        description:
          type: string
          nullable: true
        payload:
          type: object
        is_sharing_private:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        archived_at:
          type: string
          format: date-time
          nullable: true
        access:
          type: string
          enum:
            - owner
            - recipient
    CreateScenarioRequest:
      type: object
      required:
        - name
        - type
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        type:
          $ref: '#/components/schemas/ScenarioType'
        description:
          type: string
          maxLength: 2000
        payload:
          type: object
          default: {}
    UpdateScenarioRequest:
      type: object
      description: At least one field must be provided.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 2000
          nullable: true
        payload:
          type: object
        is_sharing_private:
          type: boolean
          description: Project-scoped scenarios only — 400 NOT_PROJECT_SCENARIO on a sandbox scenario.
    CloneScenarioRequest:
      type: object
      properties:
        target_project_id:
          type: string
          format: uuid
          description: Omit to create the clone as a sandbox scenario. Caller must own the target project.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: Defaults to "Copy of <original name>" when omitted.
    MoveScenarioRequest:
      type: object
      required:
        - target_project_id
      properties:
        target_project_id:
          type: string
          format: uuid
          nullable: true
          description: null moves the scenario to sandbox. Must be a project owned by the caller otherwise.
paths:
  /workspace/orgs:
    get:
      operationId: listWorkspaceOrgs
      summary: List orgs the calling user belongs to
      description: Returns every organization the caller is currently a member of.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Org memberships
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrgSummary'
                  meta:
                    type: object
        '400':
          description: Both Authorization and X-API-Key present (AMBIGUOUS_AUTH)
        '401':
          description: Missing/invalid Authorization, or X-API-Key/X-MW-Anon-Session used instead of a JWT
  /workspace/orgs/{org_id}/activate:
    post:
      operationId: activateWorkspaceOrg
      summary: Switch the caller's active organization
      description: |
        Validates membership, then atomically sets `is_active=true` for `org_id` and `false` for every other org the caller belongs to (`activate_org` RPC, migration 0050).
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
      parameters:
        - name: org_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Active org switched
        '400':
          description: AMBIGUOUS_AUTH
        '401':
          description: Auth failure
        '404':
          description: Caller is not a member of org_id (ORG_NOT_FOUND)
  /workspace/sharing/orgs:
    get:
      operationId: listWorkspaceSharingOrgs
      summary: List orgs available as sharing targets
      description: |
        Composition (spec §12 Q1): orgs whose plan != 'free', excluding the caller's own org memberships. Exposes only org id + name — never members.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Shareable orgs
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrgSummary'
                  meta:
                    type: object
        '400':
          description: AMBIGUOUS_AUTH
        '401':
          description: Auth failure
  /workspace/projects:
    get:
      operationId: listWorkspaceProjects
      summary: List projects visible to the caller
      description: |
        Owned projects (any) plus projects shared with the caller's active org (owned by other users). Each item carries `access`.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: owned_only
          in: query
          schema:
            type: boolean
            default: false
        - name: shared_only
          in: query
          schema:
            type: boolean
            default: false
        - name: archived
          in: query
          schema:
            type: boolean
            default: false
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Visible projects
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Project'
                  meta:
                    type: object
        '400':
          description: Validation error / AMBIGUOUS_AUTH
        '401':
          description: Auth failure
    post:
      operationId: createWorkspaceProject
      summary: Create a project (private by default)
      description: Creates a project owned by the caller. Private until explicitly shared.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
      responses:
        '201':
          description: Project created
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Project'
                  meta:
                    type: object
        '400':
          description: Validation error / AMBIGUOUS_AUTH
        '401':
          description: Auth failure
  /workspace/projects/{id}:
    get:
      operationId: getWorkspaceProject
      summary: Get project detail
      description: Returns the project if the caller is its owner or a recipient of an active-org share.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Project
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Project'
                  meta:
                    type: object
        '404':
          description: PROJECT_NOT_FOUND
    patch:
      operationId: updateWorkspaceProject
      summary: Update a project (owner only)
      description: Updates name/description. Only the project owner may call this.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProjectRequest'
      responses:
        '200':
          description: Updated project
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Project'
                  meta:
                    type: object
        '400':
          description: Validation error
        '403':
          description: FORBIDDEN — caller is a recipient, not the owner
        '404':
          description: PROJECT_NOT_FOUND
    delete:
      operationId: archiveWorkspaceProject
      summary: Archive a project (owner only)
      description: Soft-deletes the project (sets archived_at). Only the owner may call this.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Archived
        '403':
          description: FORBIDDEN
        '404':
          description: PROJECT_NOT_FOUND
  /workspace/projects/{id}/sharing:
    get:
      operationId: getWorkspaceProjectSharing
      summary: Get project sharing config (owner only)
      description: Returns the project's current shared-org list. Only the owner may call this.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Sharing config
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/ProjectSharing'
                  meta:
                    type: object
        '403':
          description: FORBIDDEN
        '404':
          description: PROJECT_NOT_FOUND
    patch:
      operationId: updateWorkspaceProjectSharing
      summary: Set the project's shared org list (owner only, PUT semantics)
      description: Replaces the full shared-org list. Empty array makes the project private again.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSharingRequest'
      responses:
        '200':
          description: Updated sharing config
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/ProjectSharing'
                  meta:
                    type: object
        '400':
          description: SELF_SHARE / ORG_NOT_SHAREABLE / validation error
        '403':
          description: FORBIDDEN
        '404':
          description: PROJECT_NOT_FOUND
  /workspace/projects/{project_id}/scenarios:
    get:
      operationId: listProjectScenarios
      summary: List scenarios in a project
      description: Returns scenarios visible to the caller within the given project.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archived
          in: query
          schema:
            type: boolean
            default: false
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Scenarios in the project
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '404':
          description: PROJECT_NOT_FOUND
    post:
      operationId: createProjectScenario
      summary: Create a scenario in a project (project owner only)
      description: New scenario's is_sharing_private defaults to false (inherits project sharing).
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: project_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScenarioRequest'
      responses:
        '201':
          description: Scenario created
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '400':
          description: Validation error / INVALID_SCENARIO_TYPE / PROJECT_ARCHIVED
        '403':
          description: FORBIDDEN — caller is not the project owner
        '404':
          description: PROJECT_NOT_FOUND
  /workspace/scenarios:
    get:
      operationId: listWorkspaceScenarios
      summary: List all scenarios visible to the caller (incl. sandbox)
      description: Owned scenarios (any project or sandbox) plus scenarios in shared, non-private projects.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: project_id
          in: query
          schema:
            type: string
            format: uuid
        - name: sandbox_only
          in: query
          schema:
            type: boolean
            default: false
        - name: owned_only
          in: query
          schema:
            type: boolean
            default: false
        - name: archived
          in: query
          schema:
            type: boolean
            default: false
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Visible scenarios
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '400':
          description: Validation error / AMBIGUOUS_AUTH
        '401':
          description: Auth failure
    post:
      operationId: createSandboxScenario
      summary: Create a sandbox (project-less) scenario
      description: Creates with project_id=null. Private until explicitly shared via the scenario sharing endpoint.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScenarioRequest'
      responses:
        '201':
          description: Sandbox scenario created
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '400':
          description: Validation error / INVALID_SCENARIO_TYPE
  /workspace/scenarios/{id}:
    get:
      operationId: getWorkspaceScenario
      summary: Get scenario detail
      description: Returns the scenario if the caller is its owner or a recipient with visibility into it.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Scenario
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '404':
          description: SCENARIO_NOT_FOUND
    patch:
      operationId: updateWorkspaceScenario
      summary: Update a scenario (owner only)
      description: Updates name/description/payload/is_sharing_private. Only the scenario owner may call this.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateScenarioRequest'
      responses:
        '200':
          description: Updated scenario
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '400':
          description: Validation error / NOT_PROJECT_SCENARIO
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND
    delete:
      operationId: archiveWorkspaceScenario
      summary: Archive a scenario (owner only)
      description: Soft-deletes the scenario (sets archived_at). Only the owner may call this.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Archived
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND
  /workspace/scenarios/{id}/clone:
    post:
      operationId: cloneWorkspaceScenario
      summary: Clone a scenario (owner or shared recipient)
      description: |
        The clone is always private on creation and owned by the caller — it inherits no sharing from the original.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CloneScenarioRequest'
      responses:
        '201':
          description: Cloned scenario
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '400':
          description: TARGET_PROJECT_NOT_OWNED / PROJECT_ARCHIVED / validation error
        '403':
          description: FORBIDDEN — caller has neither owner nor recipient access
        '404':
          description: SCENARIO_NOT_FOUND
  /workspace/scenarios/{id}/move:
    post:
      operationId: moveWorkspaceScenario
      summary: Move a scenario between a project and sandbox (owner only)
      description: Moving into a project resets is_sharing_private to false and drops direct org shares; moving to sandbox clears is_sharing_private and starts private.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MoveScenarioRequest'
      responses:
        '200':
          description: Moved scenario
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/Scenario'
                  meta:
                    type: object
        '400':
          description: TARGET_PROJECT_NOT_OWNED / PROJECT_ARCHIVED / validation error
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND
  /workspace/scenarios/{id}/sharing:
    get:
      operationId: getWorkspaceScenarioSharing
      summary: Get sandbox scenario sharing config
      description: Sandbox scenarios only — 400 NOT_SANDBOX_SCENARIO for project-scoped scenarios.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Sharing config
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/ProjectSharing'
                  meta:
                    type: object
        '400':
          description: NOT_SANDBOX_SCENARIO
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND
    patch:
      operationId: updateWorkspaceScenarioSharing
      summary: Set shared orgs for a sandbox scenario (PUT semantics)
      description: Sandbox scenarios only — 400 NOT_SANDBOX_SCENARIO for project-scoped scenarios.
      tags:
        - workspace
      x-credit-cost: 0
      x-motionworks-status: production
      security:
        - apiKey: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSharingRequest'
      responses:
        '200':
          description: Updated sharing config
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - meta
                properties:
                  data:
                    $ref: '#/components/schemas/ProjectSharing'
                  meta:
                    type: object
        '400':
          description: NOT_SANDBOX_SCENARIO / SELF_SHARE / ORG_NOT_SHAREABLE
        '403':
          description: FORBIDDEN
        '404':
          description: SCENARIO_NOT_FOUND
