The Editor, the Agents app and the CLI bind every workspace folder to a project by asking Govern which project its repository belongs to. The answer decides which project context, policies and rules apply in that folder.
The lookup
POST /api/v1/context/project/lookup
Authorization: Bearer <access token>
Content-Type: application/json
{"repoUrls": ["git@gitlab.example.com:group/sub/name.git", "https://gitlab.example.com/group/sub/name"]}
| Form | Use |
|---|---|
POST /api/v1/context/project/lookup with {"repoUrls": [...]} or {"projectId": "..."} | Up to 8 candidate addresses in the client's order of preference; body at most 16 KiB, exactly one JSON object |
GET /api/v1/context/project?repoUrl=...&repoUrl=... | The same lookup in a query string (up to 8 candidates) |
GET /api/v1/context/project?projectId=... | A known project |
GET /api/v1/context/project | The organisation-level context |
GET /api/v1/context/agent/project?repoUrl=... | The endpoint agent, with its device token |
Use the POST for long candidate lists: eight long addresses in a query string can exceed a proxy's request line limit.
The answer:
{
"projectId": "6f1c...",
"projectName": "Payments API",
"projectKey": "payments-api",
"spaceId": "0a9e...",
"resolvedVia": "repo_url",
"matchedCandidate": 1,
"repoKey": "gitlab.example.com/group/sub/name",
"classification": "confidential",
"spaces": [],
"sources": [],
"activeSources": 3
}
Client rule. Bind a folder only when resolvedVia is repo_url (or project_id) and projectId is set. org_fallback means no candidate is registered to a project the caller may read; the folder is not bound. The branch is recorded by clients and not used: a repository belongs to one project whatever branch is checked out.
A GET with at most one repoUrl answers in its original shape, without projectName, projectKey, matchedCandidate and repoKey, for older clients.
Errors
| Status | Code | When |
|---|---|---|
422 | context.space.invalid_request | No candidate is a repository address, more than 8 candidates, or both projectId and candidates |
409 | context.space.repo_ambiguous | The first matching candidate is a codebase source of two readable projects, or bound in the Directory to one and a codebase source of another |
429 | context.project.rate_limited | Over KEYSTONE_CONTEXT_PROJECT_LOOKUPS_PER_MINUTE (default 60 per person, GET, POST and agent route together); Retry-After given |
403 | The matched project's space (or the organisation's, for the fallback) cannot be read | |
404 | context.project.not_found | A projectId that is unknown, has no space yet, or names a project the caller cannot read |
Canonical repository address
Registration and lookup use one canonical form, compared for exact equality, never by prefix or pattern:
host/segment/.../name
| Rule | Detail |
|---|---|
| Accepted forms | https://, http://, ssh://, git:// URLs, scp-like [user@]host:path, and bare host/path. file://, local paths and other schemes are refused |
| Characters | Printable ASCII only; non-ASCII is refused everywhere, so a look-alike host can never produce another repository's key |
| Credentials | A user name is dropped only on ssh:// and scp-like addresses. A password anywhere, and any user information on https://, http:// and git://, is refused, and the error never repeats the address |
| Host | Lower-cased DNS name or IPv4 address; the port is dropped; IPv6 literals are refused |
| Path | 2 to 20 segments (GitLab subgroups supported), lower-cased; one trailing / and one trailing .git removed; empty, . and .. segments refused; only %20 decoded |
| Azure DevOps | ssh.dev.azure.com:v3/{org}/{project}/{repo} maps to dev.azure.com/{org}/{project}/_git/{repo} |
Examples: https://GitLab.com/Group/Sub/Name.git/ becomes gitlab.com/group/sub/name; git@ssh.dev.azure.com:v3/acme/My%20Project/payments becomes dev.azure.com/acme/my%20project/_git/payments.
How a registration is found
A repository is registered to a project in the Directory's project repositories, or as a codebase source of a project space. For each candidate in order, the first rule that applies decides:
- The Directory binds the key to a project whose space the caller may read: that project (unless a codebase source with the key is in another readable project:
409). - Codebase sources with the key are in exactly one readable project space: that project.
- They are in two or more readable project spaces:
409. - Otherwise the next candidate is tried.
Only readable projects take part. A repository registered to a project the caller cannot read gives exactly the answer of an unregistered one, so the lookup cannot be used to learn about projects one cannot see. Each successful lookup is audited as context.project.resolve, without the addresses.
Registration event
When the repositories of a project change (in the Directory or as codebase sources), the feed carries project.repositories_changed to readers of the project's space, with ids and counts only, never an address. Clients refresh their folder bindings with the lookup when they receive it, and after reconnecting with a resume gap. See Real-time feed.
Older registrations
Directory rows stored by earlier releases in other forms are rewritten to the canonical form by the job context.repository_backfill, every 10 minutes, per organisation, idempotently. Rows that cannot be converted, and rows that would give two projects the same key, are left unchanged and reported, as counts, in repositoryIssues of GET /api/v1/context/status (organisation administrators). The console's project page shows them and asks an administrator to fix the project's repositories.