# Workspace-Owned URL Canonicalization

> Canonical path: `specs/deployment/workspace-url-canonicalization.md`
> Status: target-spec
> Scope: public workspace-owned HTTP routes crossing `agentteam-gateway-server`,
> canonical Workspace AgentTeam Server route ownership, and maintenance-window
> activation.
> Related specs: [AgentTeam Workspace Gateway](./workspace-gateway.md),
> [Request Workspace Routing Matrix](../api/request-workspace-routing-matrix.md),
> [Workspace Route Coverage](./workspace-route-coverage-fix.md),
> [Server Profile Routing](../api/server-profile-routing.md), and
> [Daemon](../daemon/daemon.md).

## 1. Decision

Every public workspace-owned business URL uses this canonical shape:

```text
/api/v1/workspaces/:workspaceId/<workspace-owned-resource>
```

The URL workspace is the public routing key. It is not authorization. The
Gateway and target Workspace AgentTeam Server must still validate the authenticated
principal, current workspace, placement, and local ownership.

Compatibility for an old public URL without a workspace path exists only at
the Edge Gateway. A conforming Workspace AgentTeam Server registers only canonical
public workspace URLs. It does not register the old public aliases.

Embedded MeshX keeps its native `/api/v1/...` route tree as a private in-process
implementation detail. The AgentTeam forwarder strips the canonical workspace
prefix after it has authenticated and authorized the request, then calls the
native MeshX handler with a trusted `X-Workspace-Id`. This preserves one copy of
each MeshX business handler without exposing its unscoped native URL directly
from the Workspace AgentTeam Server.

Gateway and Workspace Server activate this contract together during a
maintenance window. They are one release and rollback unit; ordinary rolling
compatibility between old and new Gateway/Server versions is not part of this
contract.

## 2. Workspace authority

For a canonical request, all available workspace identities must agree:

```text
path workspace
  == X-AgentTeam-Route-Workspace-Id, when present
  == authenticated/current workspace
  == resolved credential workspace, when present
  == placement workspace routed to this owner
  == ManagedWorkspaceSet ownership on the target Server
```

The path workspace selects an upstream. It never grants membership or resource
access. The target handler continues to run its normal authorization.

Hard requirements:

1. A canonical path with an empty, malformed, or escaped workspace segment is
   rejected before proxying.
2. Conflicting path, route-header, business-header, query, credential, or
   placement identities fail closed with a stable 4xx response.
3. A missing workspace on an old URL may be recovered only from a trusted,
   unambiguous source. A user session or a default workspace group is not such a
   source.
4. `X-AgentTeam-Route-Workspace-Id` remains the temporary routing discriminator for
   old browser/SDK URLs. `X-Workspace-Id` is the backend business-context
   header, not the long-term public routing contract.
5. The Gateway never redirects an old mutation URL. It internally rewrites and
   proxies the original method, body, query, authorization, idempotency headers,
   and cancellation context.
6. The Workspace Server revalidates the canonical path even when the request
   came from a trusted Gateway.

## 3. Target request flows

### 3.1 Canonical client request

```text
client
  -> /api/v1/workspaces/wsp_1/kb/...
  -> Gateway extracts wsp_1 and resolves placement
  -> Workspace Server validates path/header/auth/owner equality
  -> forwarder strips /api/v1/workspaces/wsp_1
  -> private MeshX handler receives /api/v1/kb/... + X-Workspace-Id: wsp_1
```

### 3.2 Old URL compatibility at the Gateway

```text
old client
  -> /api/v1/kb/... + X-AgentTeam-Route-Workspace-Id: wsp_1
  -> Gateway validates wsp_1 and rewrites the path to
     /api/v1/workspaces/wsp_1/kb/...
  -> the canonical flow continues
```

If the old request has no trusted workspace discriminator, the Gateway rejects
it. It must not choose the user's default workspace or the deployment's global
MeshX owner.

### 3.3 Direct Server behavior

A direct request to a Workspace Server using an old public URL returns 404. A
direct canonical request still passes the same identity and owner checks as a
Gateway request. Internal callers use an explicit in-process transport or the
canonical URL; they do not rely on a public old-route registration.

## 4. URL migration inventory

`go run ./cmd/agentteam-route-contract` is the executable source for the registered
edge inventory. This section groups the durable migrations by route family so
new leaf endpoints inherit the same rule without adding one-off Gateway cases.

`[/*]` below means the base route and every registered descendant. The Gateway
preserves the suffix after the listed family.

### 4.1 Suffix-preserving Gateway rewrites

| Old public URL | Canonical public URL | Private upstream target |
| --- | --- | --- |
| `/api/v1/kb[/*]` | `/api/v1/workspaces/:workspaceId/kb[/*]` | `/api/v1/kb[/*]` |
| `/api/v1/memory[/*]` | `/api/v1/workspaces/:workspaceId/memory[/*]` | `/api/v1/memory[/*]` |
| `/api/v1/skills[/*]` | `/api/v1/workspaces/:workspaceId/skills[/*]` | `/api/v1/skills[/*]` |
| `/api/v1/roles[/*]` | `/api/v1/workspaces/:workspaceId/roles[/*]` | `/api/v1/roles[/*]` |
| `/api/v1/roles:from-agent` | `/api/v1/workspaces/:workspaceId/roles:from-agent` | `/api/v1/roles:from-agent` |
| `/api/v1/repository-owners` | `/api/v1/workspaces/:workspaceId/repository-owners` | `/api/v1/repository-owners` |
| `/api/v1/repository-names/available` | `/api/v1/workspaces/:workspaceId/repository-names/available` | `/api/v1/repository-names/available` |
| `/api/v1/repository-access-tokens[/*]` | `/api/v1/workspaces/:workspaceId/repository-access-tokens[/*]` | `/api/v1/repository-access-tokens[/*]` |
| `/api/v1/internal/repository-access-tokens/*` | `/api/v1/workspaces/:workspaceId/internal/repository-access-tokens/*` | `/api/v1/internal/repository-access-tokens/*` |
| `/api/v1/personal-access-tokens[/*]` | `/api/v1/workspaces/:workspaceId/personal-access-tokens[/*]` | `/api/v1/personal-access-tokens[/*]` |
| `/api/v1/automations[/*]` | `/api/v1/workspaces/:workspaceId/automations[/*]` | `/api/v1/automations[/*]` |
| `/api/v1/mcp[/*]` | `/api/v1/workspaces/:workspaceId/mcp[/*]` | `/api/v1/mcp[/*]` |
| `/api/v1/repositories[/*]` | `/api/v1/workspaces/:workspaceId/repositories[/*]` | `/api/v1/repositories[/*]` |
| `/api/v1/agent-thread/session-channel` | `/api/v1/workspaces/:workspaceId/agent-thread/session-channel` | same canonical AgentTeam handler |
| `/api/llm/*` | `/api/v1/workspaces/:workspaceId/llm/*` | existing AgentTeam LLM proxy handler |
| `/daemon/*` | `/api/v1/workspaces/:workspaceId/daemon/*` | canonical daemon handler |
| `/internal/app/runtime/*` | `/api/v1/workspaces/:workspaceId/app-runtime/*` | canonical App runtime handler |

The old MeshX and LLM families require `X-AgentTeam-Route-Workspace-Id` during the
compatibility window. Legacy daemon and App runtime calls may instead use the
workspace bound to a successfully authenticated machine or App credential.

### 4.2 Existing workspace-bearing compatibility aliases

The legacy machine family already contains a workspace, but the Gateway still
normalizes it to the v1 resource contract:

| Old public URL | Canonical public URL |
| --- | --- |
| `/api/workspaces/:workspaceId/machines[/*]` | `/api/v1/workspaces/:workspaceId/machines[/*]` |
| `.../rotate-key` | `.../api-key-rotations` |
| `.../connect-token` | `.../connect-tokens` |
| `.../evict` | `.../evictions` |
| `.../workspaces` | `.../workspace-directories` |

### 4.3 Agent Runtime requires client migration

The old Agent runtime family maps conceptually as follows:

```text
/internal/agent/:agentId/*
  -> /api/v1/workspaces/:workspaceId/agents/:agentId/runtime/*
```

The Gateway must not apply a blind suffix rewrite to this family. The old and
canonical task, status, and schedule surfaces have frozen wire differences.
The maintenance release upgrades the Agent/Desktop/CLI callers to the
canonical request and response contract. Old Agent runtime calls are rejected
after activation unless a route has an explicit, tested wire-equivalence rule.

This constraint keeps transport compatibility out of Gateway business logic
and out of the canonical Server handler.

### 4.4 Already canonical workspace URLs

The following families already carry `:workspaceId` and do not need a path
migration:

- `/api/v1/workspaces/:workspaceId/knowledge-qa/*`;
- `/api/v1/workspaces/:workspaceId/events/stream`;
- `/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/*`;
- workspace snapshot, scopes, channels, threads, messages, tasks, schedules,
  triggers, workflow, media, apps, providers, teams, reviews, and projects under
  `/api/v1/workspaces/:workspaceId/*`.

## 5. Explicit exceptions

The objective is that every **workspace-owned business URL** carries a
workspace path. It does not apply mechanically to global, protocol-defined, or
private implementation routes.

| Route family | Rule and rationale |
| --- | --- |
| Activated AGS REST, repository raw, and Git smart-HTTP operations | AgentTeam product callers carry the protocol-compatible native suffix beneath `/api/v1/workspaces/:workspaceId/ags`. Bounded ordinary-session compatibility may retain an unqualified native URL only with an authenticated active-workspace claim. Authority and target-local repository validation follow the canonical [request workspace routing matrix](../api/request-workspace-routing-matrix.md). |
| AGS `/login/device*`, `/login/oauth*`, `/auth/connected/*`, unqualified `/avatars/*`, and discovery/bootstrap routes | Non-product compatibility remains available to Standalone/`all` profiles but returns 404 at the split edge. AgentTeam `/login` and ordinary SPA history fallback remain unchanged. A repository DTO may publish the separately activated workspace-qualified avatar redirect owned by the same Workspace Server. |
| Actions runner `/_oauth/*`, `/_broker/*`, `/_runs/*` and protocol-defined `/api/v3/actions/*` | Preserve the external runner protocol. Workspace authority comes from the validated runner/session credential. |
| `/api/apps/github/webhook`, `/api/apps/gitea/webhook` | Preserve provider-configured ingress until provider registration owns a workspace-bearing callback URL. The signed delivery/subscription identity remains authoritative; the Gateway must not consume and reinterpret webhook bodies. |
| `/healthz`, `/readyz` | Process and release health are not workspace resources. |
| Central auth, billing, organization, admin, OAuth callback, and workspace-management routes | These are central or externally defined routes, not workspace data-plane URLs. |
| `/internal/admin/*` and private service endpoints | Never public through the Gateway. They use service authentication and their owning internal contract. |
| Embedded MeshX native `/api/v1/...` targets | Private in-process implementation paths reached only after canonical AgentTeam authorization and prefix stripping. |
| `/api/graphql` in split mode | Remains unsupported and fails closed until it has a storage-aware multi-owner contract. |

An exception is part of the route contract and must have a focused test. New
exceptions require an explicit identity source and cannot be created merely to
avoid migrating a client.

## 6. Gateway responsibilities

The Gateway owns public compatibility and placement routing:

1. Classify canonical workspace families before the central
   `/api/v1/workspaces/*` management fallback.
2. Extract the canonical path workspace, or resolve an old URL from the route
   header or an approved bound credential.
3. Reject missing or conflicting identities before placement lookup.
4. Resolve active placement and write the trusted route workspace header.
5. Rewrite an approved old family to its canonical URL without decoding its
   body or interpreting domain fields.
6. Strip untrusted client owner-group headers before proxying.
7. Reject unknown, internal-only, unsupported, and non-rewriteable old routes.

Rewrite rules are family-based and method-preserving. They must not contain a
leaf inventory copied from MeshX handlers.

## 7. Workspace Server and forwarder responsibilities

The Workspace AgentTeam Server:

1. registers the canonical families in section 4 and does not register their
   old public aliases;
2. validates path workspace, route header, authenticated/current workspace,
   and `ManagedWorkspaceSet` ownership;
3. preserves existing permission gates such as workspace runtime management;
4. strips exactly `/api/v1/workspaces/:workspaceId` for private MeshX targets;
5. sets `X-Workspace-Id` only from the validated canonical request context;
6. preserves method, suffix, escaped path safety, query, body, and cancellation;
7. returns 404 for a direct old public URL.

The prefix-strip adapter is owned by the forwarder route assembly. MeshX API
packages do not duplicate canonical route registration and do not learn about
Gateway compatibility.

## 8. Client and caller migration

The maintenance release updates every caller that constructs a workspace-owned
URL:

- Web MeshX resource clients, Knowledge Base helpers, Role/Skill/Memory flows,
  automation clients, automation runner registration/session/job clients,
  token management, and the shared request transport;
- Web and SDK LLM clients;
- Desktop provisioning providers, Wire/API clients, Agent runtime calls, and
  daemon persisted endpoint construction;
- CLI, MCP, smoke, E2E, release validation, and operational scripts;
- internal HTTP callers that currently bypass the Gateway.

New clients construct canonical URLs from a required, non-empty workspace ID.
They may continue to send `X-AgentTeam-Route-Workspace-Id` as a consistency hint,
but the path is the routing source. Query libraries must not issue a canonical
request until workspace selection is ready.

Direct service callers either use an explicit private in-process transport or
the canonical URL. No direct caller is allowlisted to retain an old public URL.

## 9. Maintenance-window activation and rollback

The migration is an atomic topology change, not an ordinary rolling rollout.

### 9.1 Preconditions

- The Gateway and Workspace Server artifacts come from one reviewed release
  bundle and implement the same route inventory version.
- All direct callers and Agent/Desktop/CLI clients required during the window
  are upgraded or drained.
- Placement verification reports no missing, unknown, inactive, or unroutable
  workspace.
- The old and new release bundles are both available for whole-bundle rollback.
- Smoke tests cover a non-default workspace and every route family in section 4.

### 9.2 Cutover

1. Stop public request intake, writers, background retry producers, and new
   long-connection handshakes.
2. Drain old Gateway and Workspace Server connections and confirm no old
   instance can receive new traffic.
3. Deploy the new Workspace Servers and Gateway from the same release bundle.
4. Confirm all Server and Gateway readiness endpoints before opening ingress.
5. Run the activation checks in section 10 through the final Gateway and by
   direct Server access where specified.
6. Reopen traffic only after all checks pass.

### 9.3 Rollback

Rollback restores the previous Gateway and Workspace Server bundle together
while intake remains closed. Rolling back only one component is forbidden.
After the old bundle is ready, rerun its route smoke checks before reopening
traffic.

The URL migration has no schema or data rewrite. A route failure therefore
triggers whole-bundle rollback rather than an online compatibility fallback.

## 10. Acceptance and durable guardrails

Activation requires all of the following:

1. Every section 4 canonical family reaches the correct non-default workspace
   owner through the Gateway.
2. Every approved old family reaches the same canonical Server route through a
   Gateway internal rewrite.
3. Missing, malformed, conflicting, and cross-workspace identities fail closed.
4. A direct old public URL to Workspace Server returns 404.
5. A direct canonical URL still rejects authentication and owner mismatches.
6. The forwarder proves that canonical and rewritten requests reach the same
   private MeshX method, suffix, query, and body.
7. Agent runtime tests prove canonical client wire behavior; no generic Gateway
   suffix rewrite is accepted for non-equivalent legacy routes.
8. Split-disabled AGS compatibility, workspace-placed AGS/Git, runner, webhook,
   health, central, internal-only, and unsupported routes retain the section 5
   behavior.
9. Web/Desktop/CLI route-construction tests contain no old public URL for a
   migrated family.
10. The generated route contract has no public workspace-owned business route
    without `:workspaceId` unless that route is a tested section 5 exception.

The repository guardrails must derive their inventory from registered routes,
not a manually maintained endpoint count. Adding a new workspace-owned family
without a canonical path, or adding an old public alias to Workspace Server,
must fail CI.
