Skip to content

Commit fd9e89a

Browse files
committed
feat: project templates and shareable links
Addresses the bulk of #4107 To be addressed in a follow-up: - Copying multi-user MCP servers - Preventing users from copying projects containing MCP servers they don't have access to - Preventing copied project upgrades when the user doesn't have access to MCP servers in the latest snapshot Signed-off-by: Nick Hale <[email protected]>
1 parent f1fcf97 commit fd9e89a

35 files changed

+1359
-1309
lines changed

apiclient/types/project.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ type Project struct {
99
SourceProjectID string `json:"sourceProjectID,omitempty"`
1010
UserID string `json:"userID,omitempty"`
1111
WorkflowNamesFromIntegration WorkflowNamesFromIntegration `json:"workflowNamesFromIntegration,omitempty"`
12+
TemplateUpgradeAvailable bool `json:"templateUpgradeAvailable,omitempty"`
13+
TemplateUpgradeInProgress bool `json:"templateUpgradeInProgress,omitempty"`
1214
}
1315

1416
type WorkflowNamesFromIntegration struct {

apiclient/types/template.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,15 @@ package types
22

33
type ProjectTemplate struct {
44
Metadata
5-
ProjectTemplateManifest
6-
ProjectSnapshot ThreadManifest `json:"projectSnapshot,omitempty"`
7-
MCPServers []string `json:"mcpServers,omitempty"`
8-
AssistantID string `json:"assistantID,omitempty"`
9-
ProjectID string `json:"projectID,omitempty"`
10-
PublicID string `json:"publicID,omitempty"`
11-
Ready bool `json:"ready,omitempty"`
12-
}
13-
14-
type ProjectTemplateManifest struct {
15-
Name string `json:"name,omitempty"`
16-
Public bool `json:"public,omitempty"`
17-
Featured bool `json:"featured,omitempty"`
5+
ProjectSnapshot ThreadManifest `json:"projectSnapshot,omitempty"`
6+
ProjectSnapshotRevision Time `json:"projectSnapshotRevision,omitempty"`
7+
ProjectSnapshotStale bool `json:"projectSnapshotStale,omitempty"`
8+
ProjectSnapshotUpgradeInProgress bool `json:"projectSnapshotUpgradeInProgress,omitempty"`
9+
MCPServers []string `json:"mcpServers,omitempty"`
10+
AssistantID string `json:"assistantID,omitempty"`
11+
ProjectID string `json:"projectID,omitempty"`
12+
PublicID string `json:"publicID,omitempty"`
13+
Ready bool `json:"ready,omitempty"`
1814
}
1915

2016
type ProjectTemplateList List[ProjectTemplate]

apiclient/types/zz_generated.deepcopy.go

Lines changed: 1 addition & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/authz/resources.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,9 @@ var apiResources = []string{
108108
"GET /api/assistants/{assistant_id}/projects/{project_id}/tasks/{task_id}/runs/{run_id}/files/{file...}",
109109
"POST /api/assistants/{assistant_id}/projects/{project_id}/tasks/{task_id}/runs/{run_id}/files/{file...}",
110110
"POST /api/assistants/{assistant_id}/projects/{project_id}/tasks/{task_id}/runs/{run_id}/steps/{step_id}/run",
111-
"PUT /api/assistants/{assistant_id}/projects/{project_id}/templates/{template_id}",
112-
"DELETE /api/assistants/{assistant_id}/projects/{project_id}/templates/{template_id}",
113-
"GET /api/assistants/{assistant_id}/projects/{project_id}/templates/{template_id}",
114-
"POST /api/assistants/{assistant_id}/projects/{project_id}/templates",
115-
"GET /api/assistants/{assistant_id}/projects/{project_id}/templates",
111+
"POST /api/assistants/{assistant_id}/projects/{project_id}/template",
112+
"GET /api/assistants/{assistant_id}/projects/{project_id}/template",
113+
"DELETE /api/assistants/{assistant_id}/projects/{project_id}/template",
116114
"GET /api/assistants/{assistant_id}/projects/{project_id}/threads",
117115
"POST /api/assistants/{assistant_id}/projects/{project_id}/threads",
118116
"DELETE /api/assistants/{assistant_id}/projects/{project_id}/threads/{thread_id}",
@@ -149,6 +147,7 @@ var apiResources = []string{
149147
"GET /api/assistants/{assistant_id}/projects/{project_id}/tools/{tool_id}/local-authenticate",
150148
"DELETE /api/assistants/{assistant_id}/projects/{project_id}/tools/{tool_id}/local-deauthenticate",
151149
"POST /api/assistants/{assistant_id}/projects/{project_id}/tools/{tool_id}/test",
150+
"POST /api/assistants/{assistant_id}/projects/{project_id}/upgrade-from-template",
152151
"GET /api/mcp-server-instances",
153152
"GET /api/mcp-server-instances/{mcp_server_instance_id}",
154153
"POST /api/mcp-server-instances",
@@ -293,6 +292,7 @@ func (a *Authorizer) evaluateResources(req *http.Request, vars GetVar, user user
293292
MCPID: vars("mcp_id"), // this will be either a server ID or a server instance ID
294293
PendingAuthorizationID: vars("pending_authorization_id"),
295294
ThreadShareID: vars("share_public_id"),
295+
TemplateID: vars("template_public_id"), // for /api/templates/{template_public_id} routes
296296
ToolID: vars("tool_id"),
297297
WorkspaceID: vars("workspace_id"),
298298
}

pkg/api/authz/template.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func (a *Authorizer) checkTemplate(req *http.Request, resources *Resources) (boo
1616
var templateShareList v1.ThreadShareList
1717
err := a.cache.List(req.Context(), &templateShareList, kclient.InNamespace(system.DefaultNamespace), kclient.MatchingFields{
1818
"spec.publicID": resources.TemplateID,
19+
"spec.template": "true",
1920
})
2021

2122
return len(templateShareList.Items) > 0, err

pkg/api/handlers/mcp.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ func (m *MCPHandler) ListServer(req api.Context) error {
210210
items := make([]types.MCPServer, 0, len(servers.Items))
211211

212212
for _, server := range servers.Items {
213+
if server.Annotations[v1.HideMCPServerAnnotation] == "true" {
214+
continue
215+
}
216+
213217
// Add extracted env vars to the server definition
214218
addExtractedEnvVars(&server)
215219

pkg/api/handlers/projectmcp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func convertProjectMCPServer(projectServer *v1.ProjectMCPServer, mcpServer *v1.M
5656
}
5757
pmcp.Alias = mcpServer.Spec.Alias
5858

59-
if cred != nil && mcpServer.Spec.MCPCatalogID == "" {
59+
if mcpServer.Spec.MCPCatalogID == "" {
6060
// For single-user servers, grab more status information from the MCP server.
6161
// We don't show this for shared servers, because the user can't do anything about it
6262
// if something is wrong with one of those; only the admin can.

pkg/api/handlers/projects.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,51 @@ func (h *ProjectsHandler) GetProject(req api.Context) error {
247247
return req.Write(convertProject(&thread, nil))
248248
}
249249

250+
// UpgradeFromTemplate upgrades a project to the latest snapshot of a project if the snapshot has changed since
251+
// the project was created or the last time the project was upgraded.
252+
func (h *ProjectsHandler) UpgradeFromTemplate(req api.Context) error {
253+
var (
254+
projectID = strings.Replace(req.PathValue("project_id"), system.ProjectPrefix, system.ThreadPrefix, 1)
255+
thread v1.Thread
256+
)
257+
258+
if err := req.Get(&thread, projectID); err != nil {
259+
return err
260+
}
261+
262+
if thread.Spec.SourceThreadName == "" || !thread.Spec.Project {
263+
return types.NewErrBadRequest("project was not created from a template")
264+
}
265+
266+
if !thread.Status.SourceThreadUpgradeAvailable || thread.Annotations[v1.ThreadUpgradeApprovedAnnotation] == "true" {
267+
// Project hasn't diverged from the snapshot or is in the process of upgrading, nothing to do
268+
return nil
269+
}
270+
271+
// Get the source thread to verify it's a template
272+
var source v1.Thread
273+
if err := req.Get(&source, thread.Spec.SourceThreadName); err != nil {
274+
return err
275+
}
276+
277+
// Verify the source is actually a template
278+
if !source.Spec.Template {
279+
return types.NewErrBadRequest("source project is not a template")
280+
}
281+
282+
// Project has diverged from the snapsbot, upgrade to the latest snapshot by setting the upgrade annotation
283+
if thread.Annotations == nil {
284+
thread.Annotations = map[string]string{}
285+
}
286+
thread.Annotations[v1.ThreadUpgradeApprovedAnnotation] = "true"
287+
288+
// Copy snapshot revision annotation from source template
289+
thread.Annotations[v1.ProjectSnapshotRevisionAnnotation] = source.Annotations[v1.ProjectSnapshotRevisionAnnotation]
290+
291+
// Persist spec/annotation updates first
292+
return req.Update(&thread)
293+
}
294+
250295
func (h *ProjectsHandler) ListProjects(req api.Context) error {
251296
var (
252297
assistantID = req.PathValue("assistant_id")
@@ -469,6 +514,8 @@ func convertProject(thread *v1.Thread, parentThread *v1.Thread) types.Project {
469514
Editor: thread.IsEditor(),
470515
UserID: thread.Spec.UserID,
471516
WorkflowNamesFromIntegration: thread.Status.WorkflowNamesFromIntegration,
517+
TemplateUpgradeAvailable: thread.Status.SourceThreadUpgradeAvailable || thread.Annotations[v1.ThreadUpgradeApprovedAnnotation] == "true",
518+
TemplateUpgradeInProgress: thread.Status.UpgradeInProgress,
472519
}
473520

474521
// Include tools from parent project

0 commit comments

Comments
 (0)