# Canonical Projects in retained Themelio The Project selector reads `GET /api/v1/me/projects` through the generated API client. It follows every page, retains canonical UUIDs and revisions, and reports failed reads instead of presenting a partial directory. Duplicate display names do not merge Projects. The six retained `/api/{category}/projects` aliases return the same canonical member directory; they no longer list GitLab subgroups. Beto owns the Project aggregate in PostgreSQL. Its current source-managed slice binds each canonical Project to an exact Waldur installation and external Project ID. Waldur still supplies metadata and membership observations. Neither the Waldur UUID nor a GitLab group ID is the canonical UUID. See the [Project context](../../../../../services/beto/contexts/project/README.md). ## Terms The retained web uses one word for each of these concepts: | Term | Meaning | In code and the web API | | --- | --- | --- | | Project | The canonical Project: a UUID with members and allocated resources, owned by Beto. | `projectId` for the UUID, `project` for a Project object | | Repository | A model, dataset, Space, repository, recipe or upskilling. GitLab stores each one as a GitLab project. | `repository`; `gitlabProject` where code handles the GitLab object | | Project group | The GitLab group `/` and its per-category subgroups, provisioned by Source Control. | `gitlabGroup` | "Project" never means a GitLab project. GitLab's own `namespace` fields keep their provider names. ## Repository addressing Browser and HTTP routes address a repository as `/{category}/{projectId}/{repository}`, where `projectId` is the canonical Project UUID; display names are labels. Repository lists take the Project filter as `?project=` in browser URLs and as `?projectId=` on `/api/`. The GitLab adaptor maps a repository to: ```text /// ``` `SOURCE_CONTROL_GITLAB_ROOT_GROUP` defaults to `pharos-projects`; a different installation must set the same value in the retained web and Source Control. Categories are `models`, `datasets`, `spaces`, `repos`, `recipes` and `upskillings`. The web resolves existing groups with the caller's GitLab token. It never creates Project or category groups. Source Control owns their provision and membership projection. Member listing uses GitLab's complete `projects(membership: true)` inventory and validates each canonical path; it does not require access to the private root group. Listing follows GitLab cursors before filtering and offset pagination. Detail, README, metadata, tree, blob, commit lookup, settings and actions-file requests use the same mapping. Deletion uses Source Control’s canonical API below. Blob URLs encode each filename segment separately and retain literal slashes between folders. Every category uses its catch-all blob route; the `ref` query parameter selects the revision. The commits endpoint retains its per-file response when `paths` is present. Without `paths`, it returns ordered history with explicit page metadata and an immutable `revision` for subsequent pages. Missing repositories and revisions remain errors. The shared file browser exposes history on member and public repository pages, retains the selected revision and file path in the URL, and provides a return to the current revision. History and file reads use the same private-authoring/public-copy permission boundary described below. Creation and fork destinations must be in the canonical member directory and remain subject to native GitLab permissions. Fork targets intersect that directory with all pages of GitLab's allowed targets. Create and fork responses include an explicit UI `route`; physical GitLab paths are not UI routes. Clone commands use the physical GitLab path through the existing compatibility Git proxy. This does not introduce the planned token broker or replace retained Git authentication. ### Public publication Source Control publishes selected repository content and Git history into `pharos-public///`. The private authoring repository retains its canonical identity and write path. The public copy has no Project group share or copied Project membership. Publication lifecycle, provider bindings and recovery belong to the [Source Control context](../../../../../services/metallo/contexts/source_control/README.md). Creation always writes the private repository and initial README first. A Public selection then sends the authorised publication command. The card and settings show `private`, `pending`, `published` or `refused`; explicit Refresh checks progress. Publication and withdrawal use the observed `publication_version` and a UUID idempotency key. An ambiguous delivery retains the same command while the view stays open. A creation response that cannot confirm publication links to the created repository with a visible status-check notice. The catalogue combines the member inventory and the anonymous public inventory, deduplicating the logical route and removing reserved publication topics. Content reads prefer accessible private authoring; an inaccessible repository falls back to the public copy without forwarding a provider credential. A missing file in an accessible authoring repository stays missing. Anonymous README, metadata, tree, blob and commit reads follow an explicit route allowlist. Forking a public copy creates a private repository in the selected member Project. Public clone commands use the public copy and require no token. Public detail pages render the repository's README body, or its frontmatter description, alongside metadata and files. The initial README read uses the same resource revision as the file browser and exposes no editing control. Missing README files and unavailable reads have distinct visible states. These reads use the existing publication allowlist and anonymous provider binding. ## Account preparation and Project information Account preparation proves the exact federated identity, establishes its Party binding and reconciles retained credentials. It neither creates nor requires personal category groups or Owner memberships. Existing personal groups are left untouched. AUP and authentication fences remain in force. Project detail reads the canonical member directory and two owner-backed queries: `GET /api/v1/projects/{projectId}/members` reads Beto’s complete membership, canonical Party IDs, roles, Project revision and unresolved-member count. `GET /api/v1/projects/{projectId}/allocations` reads Resource Provisioning’s complete saved allocations, revisions, lifecycles, observation times and configured/applied limits. Applied limits remain null until a Site confirms them. Member names require a separate Party profile contract; this view labels members by their Party IDs. Both queries check current `project.view` authority and report failures explicitly. Allocation limits display CPU cores, binary memory/storage units and GPU device counts. A failed details read offers an explicit retry; it does not poll. Profile settings retain the authenticated name and email separately from editable Party-owned profile fields. The profile editor uses versioned, idempotent saves through the canonical Party Governance API. Profiles remain private until their owner opts in to public visibility. Repository listings combine accessible member repositories with explicitly published public copies. See the [profile contract](../../../../../contracts/party_governance/http_api/README.md). Deployment setup requests a GPU count; Execution Core checks that demand against the provisioned account. Native GPU labels are not action inputs. The Kubeflow link opens its landing page without guessing its namespace. Execution admission and native workspace mapping remain owned by the existing execution contexts. ## Runs through the new API Retained action, job and deployment routes forward the API session to the generated Execution Core HTTP endpoints. They never mint a GitLab PAT, read a provider user ID or send a canonical Project ID as a native workspace. Workload Portfolio prepares the immutable action revision; Execution Core resolves admission and the native account through Resource Provisioning. The current API chooses the matching provisioned account; this change adds no browser allocation picker. A Project's job and deployment listings are `GET /api/{jobs,deployments}/projects/{projectId}`. They forward to the Go API's `/api/v1/{jobs,deployments}/namespaces/{namespace}`, which keeps Themelio's earlier path, and its `namespace` parameter, until the Execution Core contract is renamed. Launch sends a browser-generated `Idempotency-Key` and `If-Match: "0"`. Retry after an ambiguous failure retains the same identity and exact body while the view stays open. Cancellation retains the Run UUID and observed revision. A definite version conflict triggers a fresh authorised read and a new Stop command, with at most three submissions per user action. A network failure or unavailable owner preserves the pending Stop's original revision and command identity, even if realtime has advanced the displayed Run. An already requested stop completes the interaction. The outcome identifies the Launch, not the eventual Run, so acceptance opens the list rather than fabricating a Run detail URL. A second submission after confirmed acceptance is an intentional new launch. The UI displays owner statuses, waiting/refusal explanations and stop requests. It refreshes on typed Protobuf Run publications over server-authorised Centrifugo subscriptions, rejects mismatched audiences and old revisions, and reconciles lost history through an authorised read. It has no timer polling or client subscriptions. Explicit Refresh remains available, including while a Launch awaits planning. Logs and public service addresses retain the current owner's documented limitations. The direct inference endpoint and legacy pod callbacks return 410. Legacy numeric Run IDs are not imported or translated. The actions document validator checks structure and rejects retired shared `ref` definitions, GPU labels, variations and cached checkouts; the owner validates the complete action and admission when launching. Its result must not be treated as an admission decision. Repository creation and deployment setup write explicit Gradio, Streamlit or Hugging Face actions. Space commands run in the checked-out `main_repo`, use the Runtime virtualenv and listen on port 8000. Commit pinned dependencies in `requirements.txt` for reproducible applications; without that file, setup installs the named framework. The Hugging Face runtime serves the model from the same repository. No shared-reference resolver or root actor participates. The integrated legacy UI restores the root and authenticated layouts against these canonical APIs. The server-rendered selector preserves Project query failures. Deployment replica counts remain unchanged. The base Run deployments have zero replicas; the dev-box overlay enables them for qualification. Inspect the rendered configuration and live deployment on the target. See the authoritative [Run endpoint guide](../../../../../services/metallo/docs/epics/instantiate-and-run/runtime-endpoints.md). ## Existing content and rollout This change migrates application addressing. It does not transfer or delete live repositories or groups, infer Project ownership from names, or supply redirects for ambiguous old slugs. Before deployment: 1. Qualify Beto's member directory and Source Control's Project groups on the target, including removal of access. A visible Project can precede completion of its Project group projection. 2. Inventory old repositories by immutable GitLab project ID and record an explicitly reviewed mapping to canonical Project ID, category and name. Personal repositories need an ownership decision; username equality is not one. 3. Check destination collisions, inherited visibility/permissions, LFS objects, branch protections, webhooks and external clone URLs. Transfer only mapped repositories with the provider's supported operation, retaining their native IDs. Confirm Source Control rediscovers each repository at the new location. Rewrite explicit `repos[].url` values in `actions.yaml` to the configured GitLab HTTPS origin and canonical path within the same Project. Old personal paths and compatibility-proxy URLs are not accepted. Inline legacy shared `ref` actions and remove unsupported options; transferring a repository does not rewrite its files. Qualify each converted action through the Run API. 4. Coordinate writes during transfer and deployment. Verify clone/fetch/push, LFS, listing, creation, fork, README/actions and denied access before reopening writes. Retain the inventory and transfer outcomes for recovery. Update callers using old UI URLs; do not silently fall back to personal groups. 5. Qualify launch, ambiguous retry, pending admission, access revocation, cancellation conflicts and realtime recovery against the real owners before deploying the integrated UI or enabling any paused request host/workers. Inventory any old actor-managed workloads and drain or migrate them through an explicit operational procedure; the new API cannot stop them by their numeric IDs. 6. Remove empty legacy groups only under a separate, reviewed cleanup after checking contents, memberships and external references. ## Regression checks `pnpm --dir compat/themelio/services/web test` exercises canonical Projects beside a legacy personal-group fixture, all Project pages, duplicate names, owner failure, all six repository categories, paginated fork destinations, creation, README and actions writes/reads and deletion. Root-actor tests prove new and returning users prepare successfully when every group operation is rejected. The assembled authentication fixture asserts no group or membership creation; live qualification checks the absence of personal category groups for its fresh disposable identity. Creation-route fixtures cover GitLab storage conflicts and validation messages across all six categories, preserving HTTP status, redacting credentials and rejecting raw diagnostic bodies. They also verify bounded fallback messages and that a rejected creation causes no follow-up provider writes. A controlled browser fixture mounts the six real new-repository pages and `RepositoryForm`. It runs the production create route against a local fake GitLab and Project owner, so failed creations reach the visible alert with one write, the entered name and Project retained, no navigation and an explicit successful retry. It is fixture proof, not live GitLab or Icarus qualification. Run it from the repository root with `pnpm exec playwright test --config acceptance/browser/repository-creation-errors.config.ts`. Run regressions exercise launch/read/stop forwarding, session and origin checks, UUIDs, preserved retry keys and versions, owner failures, field errors, refusal outcomes and typed publication audience/revision checks. Architecture negative fixtures reject root-actor imports, token minting, polling and browser subscriptions in the Run path. Shared `scripts/fixtures/repository-actions.json` documents are asserted against actual web writes and passed through Metallo Workload conversion and execution validation (`tests/workload_portfolio/test_web_action_templates.py`). Run the assembled authentication lane on the configured dev box, plus the live browser/provider checks before deployment. Unit fixtures do not qualify a deployed GitLab installation or migrate existing content. ## Repository deletion Developers retain GitLab Developer and Project administrators retain Maintainer. The UI resolves a path once with `GET /api/v1/projects/{projectId}/repositories/resolve?kind=…&name=…`, then addresses `GET` and `DELETE /api/v1/projects/{projectId}/repositories/{repositoryId}` by its canonical repository ID. The response includes `can_delete`, `deletion_version` and `deletion_state`. Deletion requires Beto’s current `repositories.delete` capability, granted to Project administrators, with a UUID `Idempotency-Key` and `If-Match` containing the observed deletion version (zero initially). Source Control durably records a pending request before calling GitLab with its existing scoped service credential. It rechecks authority and the saved native repository’s Project before deletion. Retries retain the command input and native ID; reusing a path cannot retarget the request. The browser preserves an ambiguous attempt and offers a manual status check. States are `none`, `pending`, `scheduled` and `refused`; a refusal can be retried with a new key and current version after access is restored. `scheduled` means GitLab accepted deletion; GitLab’s retention period still applies. The legacy `/api/repos/delete` returns 410. ## Migration note correction Part 1 keeps the Project-first GitLab layout from #995: `pharos-projects///`. Source Control continues to provision these Project/category groups and project members. Themelio adapts its retained `///` routes by using the canonical Project UUID as `namespace` and translating the provider path at its GitLab boundary. It takes Projects from `/api/v1/me/projects`. Creation, catalogue, reads, edits and forks use that mapping. There is no migration to physical `/` groups. Part 2’s future create, list, README, branches, actions, tree, file, fork and short-lived Git-grant APIs remain separate work. The metadata/resolve/deletion operations above now cover repository identity, deletion and publication. Project members and allocations now use their owners, so their earlier “keep legacy for review” entries are superseded. #975’s Pipeline API is included through the PR stack. The [owner API qualification report](../../../../../docs/qualification/project-owner-apis-995.md) records the tested revisions, infrastructure/provider checks and deployment limits.