# Video Engine API: Project-Scoped Endpoints ## Overview The RoboCo video engine API is CEO-only and manages three concerns: 1. **On-demand video requests**: `POST /api/video/request` opens a video-authoring task scoped to a specific project 2. **Re-render (CEO retry)**: `POST /api/video/pipeline/{task_id}/rerender` clears render idempotency keys to re-trigger rendering 3. **Live preview proxy**: `GET /api/video/preview/{task_id}/{file_path:path}` serves authoring task composition HTML + assets with path-traversal confinement All endpoints are CEO-only and require the global video engine flag enabled (`ROBOCO_VIDEO_ENGINE_ENABLED`). --- ## Endpoint: POST /api/video/request ### Purpose Open a UX/UI video-authoring task for the CEO's on-demand brief, scoped to a specific project. ### Authentication CEO-only (401 if not CEO). ### Request Body (VideoRequestBody) ```json { "occasion": "string", // Unique identifier; required, min 1 char "brief": "string", // Video brief description; required, min 1 char "platforms": ["string"], // Target platforms: ["x", "tiktok"]; required, min 1 "project_id": "UUID" // Project to author against; required (NEW in v2) } ``` **Breaking Change**: `project_id` is now **required**. This field scopes the video authoring task and its render pass to the specified project, replacing the hardcoded `self_heal_project_slug` behavior. ### Response (VideoRequestResponse) ```json { "status": "opened|disabled|not_opened", "task_id": "UUID|null", "detail": "string" } ``` ### Response Codes | Code | Status | Meaning | |------|--------|---------| | 200 | opened | Task created and dispatched to UX/UI developer | | 200 | disabled | Video engine is off (`ROBOCO_VIDEO_ENGINE_ENABLED=false`) | | 200 | not_opened | Duplicate occasion or open-post cap reached | | 404 | — | `project_id` unresolvable OR project not opted in (`video_engine_enabled=false`) | | 401 | — | Not authenticated as CEO | ### Behavior 1. **Project validation**: Looks up `project_id` and checks `video_engine_enabled=true`. Returns 404 if unresolvable or not opted in. 2. **Task creation**: Opens a normal ASSIGNED delivery task (`source=video`) dispatched to an available UX/UI developer (balanced by open-task count). 3. **Duplicate check**: Returns `not_opened` if a task for this `occasion` is already open. 4. **Open-post cap**: Returns `not_opened` if open-task count ≥ `ROBOCO_VIDEO_MAX_OPEN_POSTS`. ### Example ```bash curl -X POST http://localhost:3000/api/video/request \ -H 'X-Agent-Token: ' \ -H 'Content-Type: application/json' \ -d '{ "occasion": "v2.0 Launch", "brief": "30-second teaser for new dashboard", "platforms": ["x", "tiktok"], "project_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` --- ## Endpoint: POST /api/video/pipeline/{task_id}/rerender ### Purpose Clear render idempotency keys (`render_status`, `render_attempts`, `render_error`) on a completed video-authoring task, triggering the render loop to re-pick it up on the next cycle. **Use case**: CEO fixes a composition error or wants to retry past a `failed` terminal state. ### Authentication CEO-only (401 if not CEO). ### Path Parameters | Name | Type | Description | |---|---|---| | `task_id` | UUID | Video-authoring task ID | ### Response (VideoPipelineItemResponse) ```json { "task_id": "UUID", "title": "string", "occasion": "string", "status": "string", "pr_number": "int|null", "composition_id": "string|null", "render_status": "string|null", "render_attempts": "int", "max_attempts": "int", "render_error": "string|null" } ``` After clearing, `render_status`, `render_attempts`, and `render_error` are `null` or zero. ### Response Codes | Code | Meaning | |------|---------| | 200 | Keys cleared; next render cycle re-picks this task | | 404 | Task not found, not video task, not completed, or no `composition_id` (nothing to render) | | 401 | Not authenticated as CEO | ### Behavior 1. **Validation**: Checks task exists, is a video-authoring task (`source=VIDEO_SOURCE`), is COMPLETED, and has a `composition_id`. 2. **Clear keys**: Removes `render_status`, `render_attempts`, `render_error` from `video_draft` marker; preserves other fields. 3. **Render loop pickup**: On next orchestrator cycle, render loop scans for tasks with `render_status` unset and re-renders. ### Example ```bash curl -X POST http://localhost:3000/api/video/pipeline/550e8400-e29b-41d4-a716-446655440000/rerender \ -H 'X-Agent-Token: ' ``` --- ## Endpoint: GET /api/video/preview/{task_id}/{file_path:path} ### Purpose Serve a video-authoring task's composition HTML and sibling assets (kit/, public/, etc.) from the project's merged read-clone. Used by the panel's live preview iframe. ### Authentication CEO-only (401 if not CEO). ### Path Parameters | Name | Type | Description | |---|---|---| | `task_id` | UUID | Video-authoring task ID | | `file_path` | string | Path relative to workspace root; e.g., `motion/compositions//vertical.html` | ### Response - **Content-Type**: Auto-detected from file extension - **Headers**: - `X-Frame-Options: SAMEORIGIN` — Allows same-origin iframe embedding - `Content-Security-Policy: frame-ancestors 'self'` — Restricts frame embedding to same origin - **Body**: File contents (HTML, CSS, JS, images, etc.) ### Response Codes | Code | Meaning | |------|---------| | 200 | File served successfully | | 404 | Task/project not found, file doesn't exist, or file path escapes workspace root | | 401 | Not authenticated as CEO | ### Behavior 1. **Task lookup**: Fetches task; validates it's a video task (`source=VIDEO_SOURCE`) with `project_id`. 2. **Project resolution**: Looks up project by `project_id`. 3. **Workspace fetch**: Ensures project's read-clone is available (clones if needed). 4. **Path resolution**: - Strips leading `/` from `file_path` - Resolves against workspace root - Validates resolved path is under root and is a regular file - Returns 404 on traversal attempt or non-file path 5. **Serve**: Returns file with iframe-permitting headers. ### Path Traversal Confinement The `_resolve_preview_path` helper prevents directory-traversal attacks: ```python candidate = (root / file_path.lstrip("/")).resolve() if not candidate.is_relative_to(root) or not candidate.is_file(): return None ``` Guarantees: - `../` sequences are resolved before confinement check - Absolute paths don't escape (resolved relative to root) - Symlinks are resolved and still held under confinement - Only regular files served; directories return 404 ### Example ```bash # Serve composition HTML curl -H 'X-Agent-Token: ' \ 'http://localhost:3000/api/video/preview/550e8400-e29b-41d4-a716-446655440000/motion/compositions/my-id/vertical.html' # Serve referenced asset curl -H 'X-Agent-Token: ' \ 'http://localhost:3000/api/video/preview/550e8400-e29b-41d4-a716-446655440000/kit/public/logo.png' ``` --- ## Project-Scoping Architecture ### What Changed Previously, the video engine hardcoded `settings.self_heal_project_slug` everywhere. Now: 1. **On-demand requests** (`POST /video/request`): `project_id` required in request body 2. **Authoring tasks**: Each task stores its own `project_id` 3. **Render loop** (`orchestrator._render_both_cuts`): Uses task's `project_id` to resolve motion/ workspace, not hardcoded setting ### Rationale Each opted-in project can now: - Author and render videos against its own `motion/` directory - Participate in release and spotlight videos from its own codebase - Support on-demand briefs scoped to specific projects ### Resolution Method New `VideoEngine.resolve_authoring_project(project_id, occasion)`: - **If `project_id` provided** (on-demand, per-task): Looks up project by ID - **If `project_id` is None** (release/spotlight hooks): Falls back to fixed RoboCo project (`self_heal_project_slug`) - **Both paths**: Check `video_engine_enabled` and log skip reasons identically ### Migration Impact **For on-demand endpoint clients**: - Must now supply `project_id` in request body - Requests without `project_id` fail validation (422) - Panel's video-request form needs project picker (frontend task, out of scope) **For release/spotlight hooks**: - No change; they continue defaulting to fixed RoboCo project when no `project_id` provided