Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
61c38b4
chore: initialize multi-agent coordination plan
claude Nov 19, 2025
633cf2d
feat(tests): add integration test infrastructure for Agent 3 Validator
claude Nov 19, 2025
8d9552b
feat(architect): complete Phase 5.5 research and create multi-agent plan
claude Nov 19, 2025
64cb3b6
feat(architect): add critical applications and sessions issues to plan
claude Nov 19, 2025
5ffb1f8
feat(architect): remove false positives and add plugin architecture docs
claude Nov 19, 2025
8ffdd84
Merge architect plan into validator branch
claude Nov 19, 2025
6ecc9a5
Merge Architect's Phase 5.5 plan
claude Nov 19, 2025
aeef1ae
docs: add Phase 5.5 documentation outlines
claude Nov 19, 2025
c08ff10
feat(tests): add comprehensive test plans for Phase 5.5 validation
claude Nov 19, 2025
f964a02
fix: resolve 8 critical platform issues for Phase 5.5
claude Nov 19, 2025
fad20bb
docs: update plan with Builder completion of 8 critical fixes
claude Nov 19, 2025
4b29be0
fix(architect): update timeline to match actual task backlog
claude Nov 19, 2025
7a50933
Merge remote-tracking branch 'origin/claude/streamspace-architect-res…
claude Nov 19, 2025
0d2706c
docs: add Scribe status update to multi-agent plan
claude Nov 19, 2025
a156bf4
feat(tests): implement core platform and security integration tests
claude Nov 19, 2025
dc5cd5a
docs: update plan with Validator test implementation progress
claude Nov 19, 2025
996e6e4
fix: resolve 3 HIGH priority issues for Phase 5.5
claude Nov 19, 2025
1b22dd4
docs: update plan with Builder completion of HIGH priority fixes
claude Nov 19, 2025
418430e
feat(tests): implement plugin system integration tests
claude Nov 19, 2025
454de79
docs(architect): add agent branch information for coordination
claude Nov 19, 2025
2a8f4d9
docs: update plan with complete Validator test implementation summary
claude Nov 19, 2025
8220906
Merge latest architect updates into validator branch
claude Nov 19, 2025
9e3acd1
Merge remote-tracking branch 'origin/claude/streamspace-architect-res…
claude Nov 19, 2025
f83f078
feat(tests): implement batch operations integration tests
claude Nov 19, 2025
5ff2efa
docs: update plan with final Validator test implementation (22 tests)
claude Nov 19, 2025
0f31451
fix: complete all MEDIUM priority issues for Phase 5.5
claude Nov 19, 2025
e2bf6be
fix: UI security and cleanup improvements
claude Nov 19, 2025
630e36e
docs: update multi-agent plan with MEDIUM priority and UI fix progress
claude Nov 19, 2025
6fd6235
feat(architect): add implementation code for complex design decisions
claude Nov 19, 2025
638cb54
Merge remote-tracking branch 'origin/claude/streamspace-architect-res…
claude Nov 19, 2025
6243ae6
Merge remote-tracking branch 'origin/claude/streamspace-architect-res…
claude Nov 19, 2025
494ba5b
docs: update documentation with Architect design decisions
claude Nov 19, 2025
cb27da5
feat: implement Dashboard favorites using backend API
claude Nov 19, 2025
ccf4dad
docs: update Scribe status and documentation progress to 50%
claude Nov 19, 2025
7d49e54
feat(tests): add test execution scripts and report template
claude Nov 19, 2025
2b14d00
docs: update plan - all UI fixes complete including Dashboard favorites
claude Nov 19, 2025
c0fbf26
docs: update plan with test execution scripts (95% progress)
claude Nov 19, 2025
aecc8c5
feat(architect): add design decisions for all critical and high-prior…
claude Nov 19, 2025
c760b0d
Merge remote-tracking branch 'origin/claude/streamspace-architect-res…
claude Nov 19, 2025
c4d0de4
Merge Builder's completed implementations into Scribe branch
claude Nov 19, 2025
f050a78
docs: mark Phase 5.5 ready for testing - all 19 actionable issues res…
claude Nov 19, 2025
a7bfbe9
Merge Builder fixes for validation testing
claude Nov 19, 2025
be5a555
docs: finalize Phase 5.5 documentation - 100% complete
claude Nov 19, 2025
cd6110f
fix(tests): resolve compilation errors in integration tests
claude Nov 19, 2025
5eb1618
docs: update plan - testing at 100% with all tests compiled
claude Nov 19, 2025
2095914
feat(architect): add design decisions for medium-priority issues
claude Nov 19, 2025
316b5a1
feat(architect): add design decisions for UI fixes (Week 5)
claude Nov 19, 2025
0d77bb7
Merge Builder's final Phase 5.5 completion
claude Nov 19, 2025
b5cb7dd
Merge Architect's design decisions
claude Nov 19, 2025
cc738b0
Merge Scribe documentation and resolve conflicts
claude Nov 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,283 changes: 2,283 additions & 0 deletions .claude/multi-agent/MULTI_AGENT_PLAN.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ func main() {
notificationsHandler := handlers.NewNotificationsHandler(database)
searchHandler := handlers.NewSearchHandler(database)
// NOTE: Session snapshots now handled by streamspace-snapshots plugin
sessionTemplatesHandler := handlers.NewSessionTemplatesHandler(database)
sessionTemplatesHandler := handlers.NewSessionTemplatesHandler(database, k8sClient, eventPublisher, platform)
batchHandler := handlers.NewBatchHandler(database)
monitoringHandler := handlers.NewMonitoringHandler(database)
quotasHandler := handlers.NewQuotasHandler(database)
Expand All @@ -327,7 +327,7 @@ func main() {
securityHandler := handlers.NewSecurityHandler(database)
templateVersioningHandler := handlers.NewTemplateVersioningHandler(database)
setupHandler := handlers.NewSetupHandler(database)
applicationHandler := handlers.NewApplicationHandler(database, eventPublisher, platform)
applicationHandler := handlers.NewApplicationHandler(database, eventPublisher, k8sClient, platform)
// NOTE: Billing is now handled by the streamspace-billing plugin

// SECURITY: Initialize webhook authentication
Expand Down
66 changes: 60 additions & 6 deletions api/internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,13 +548,14 @@ func (h *Handler) CreateSession(c *gin.Context) {
}

// Generate session name: {user}-{template}-{random}
sessionName := fmt.Sprintf("%s-%s-%s", req.User, req.Template, uuid.New().String()[:8])
// Use resolved templateName (from applicationId lookup or req.Template)
sessionName := fmt.Sprintf("%s-%s-%s", req.User, templateName, uuid.New().String()[:8])

session := &k8s.Session{
Name: sessionName,
Namespace: h.namespace,
User: req.User,
Template: req.Template,
Template: templateName,
State: "running",
}

Expand Down Expand Up @@ -596,12 +597,34 @@ func (h *Handler) CreateSession(c *gin.Context) {
createEvent := &events.SessionCreateEvent{
SessionID: sessionName,
UserID: req.User,
TemplateID: req.Template,
TemplateID: templateName,
Platform: h.platform,
Resources: events.ResourceSpec{Memory: memory, CPU: cpu},
PersistentHome: session.PersistentHome,
IdleTimeout: session.IdleTimeout,
}

// Add template configuration for Docker controller
if template != nil {
vncPort := 3000 // Default VNC port
if template.VNC != nil && template.VNC.Port > 0 {
vncPort = int(template.VNC.Port)
}

// Convert env vars to map
envMap := make(map[string]string)
for _, env := range template.Env {
envMap[env.Name] = env.Value
}

createEvent.TemplateConfig = &events.TemplateConfig{
Image: template.BaseImage,
VNCPort: vncPort,
DisplayName: template.DisplayName,
Env: envMap,
}
}

if err := h.publisher.PublishSessionCreate(ctx, createEvent); err != nil {
log.Printf("Warning: Failed to publish session create event: %v", err)
}
Expand Down Expand Up @@ -739,11 +762,29 @@ func (h *Handler) ConnectSession(c *gin.Context) {
return
}

// Determine session readiness and URL availability
sessionUrl := session.Status.URL
message := "Connection established."
ready := true

if session.State == "hibernated" {
message = "Connection established. Session is waking from hibernation - please wait."
ready = false
} else if session.State == "pending" {
message = "Connection established. Session is starting up - please wait."
ready = false
} else if sessionUrl == "" {
// Session is running but URL not yet available (pod still initializing)
message = "Connection established. Waiting for session endpoint - please wait."
ready = false
}

c.JSON(http.StatusOK, gin.H{
"connectionId": conn.ID,
"sessionUrl": session.Status.URL,
"sessionUrl": sessionUrl,
"state": session.State,
"message": "Connection established. Session will auto-start if hibernated.",
"ready": ready,
"message": message,
})
}

Expand Down Expand Up @@ -776,18 +817,31 @@ func (h *Handler) DisconnectSession(c *gin.Context) {
func (h *Handler) SessionHeartbeat(c *gin.Context) {
// SECURITY FIX: Use request context for proper cancellation and timeout handling
ctx := c.Request.Context()
sessionID := c.Param("id")
connectionID := c.Query("connectionId")

if connectionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "connectionId parameter required"})
return
}

if err := h.connTracker.UpdateHeartbeat(ctx, connectionID); err != nil {
// Validate that the connection belongs to the specified session
conn := h.connTracker.GetConnection(connectionID)
if conn == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Connection not found"})
return
}

if conn.SessionID != sessionID {
c.JSON(http.StatusForbidden, gin.H{"error": "Connection does not belong to this session"})
return
}

if err := h.connTracker.UpdateHeartbeat(ctx, connectionID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update heartbeat"})
return
}

c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

Expand Down
54 changes: 50 additions & 4 deletions api/internal/auth/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ import (
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/crewjam/saml"
Expand All @@ -114,6 +115,53 @@ import (
"github.com/streamspace/streamspace/api/internal/models"
)

// validateReturnURL validates that a return URL is safe to redirect to.
//
// Security Considerations:
// - Only allows relative URLs (starting with /)
// - Prevents protocol-relative URLs (//evil.com)
// - Prevents URLs with multiple slashes that could be exploited
// - Returns "/" as safe default if validation fails
//
// This prevents open redirect vulnerabilities where an attacker could
// craft a URL like ?return_url=//evil.com/steal-token to redirect
// users to malicious sites after authentication.
func validateReturnURL(returnURL string) string {
// Default to home page
if returnURL == "" {
return "/"
}

// Must start with a single slash (relative path)
if !strings.HasPrefix(returnURL, "/") {
return "/"
}

// Prevent protocol-relative URLs (//evil.com)
if strings.HasPrefix(returnURL, "//") {
return "/"
}

// Prevent URLs that could be manipulated
// e.g., /\evil.com on some servers
if strings.ContainsAny(returnURL, "\\") {
return "/"
}

// Prevent URLs with scheme-like patterns
if strings.Contains(returnURL, "://") {
return "/"
}

// Prevent URLs with encoded characters that could be exploited
// after being decoded by the browser
if strings.Contains(returnURL, "%2f") || strings.Contains(returnURL, "%2F") {
return "/"
}

return returnURL
}

// AuthHandler handles authentication requests
type AuthHandler struct {
userDB *db.UserDB
Expand Down Expand Up @@ -303,10 +351,8 @@ func (h *AuthHandler) SAMLLogin(c *gin.Context) {
}

// Store return URL in cookie for post-login redirect
returnURL := c.Query("return_url")
if returnURL == "" {
returnURL = "/"
}
// SECURITY: Validate return URL to prevent open redirect attacks
returnURL := validateReturnURL(c.Query("return_url"))

// Set secure cookie with return URL (1 hour expiration)
c.SetCookie(
Expand Down
11 changes: 4 additions & 7 deletions api/internal/auth/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -1258,22 +1258,19 @@ func (sa *SAMLAuthenticator) SetupRoutes(router *gin.Engine) {
// - return_url (optional): Where to redirect after authentication
// Default: "/"
//
// SECURITY NOTE: return_url should be validated to prevent open redirects
// TODO: Add whitelist validation for return_url
// SECURITY: return_url is validated to prevent open redirect attacks
samlGroup.GET("/login", func(c *gin.Context) {
// STEP 1: Get return URL from query parameter
// This is where user will be redirected after successful authentication
returnURL := c.Query("return_url")
if returnURL == "" {
returnURL = "/" // Default to home page
}
// SECURITY: Validate to prevent open redirect attacks
returnURL := validateReturnURL(c.Query("return_url"))

// STEP 2: Store return URL in cookie
// The ACS endpoint will read this cookie and redirect user after auth
//
// Cookie parameters:
// - Name: "saml_return_url"
// - Value: returnURL (e.g., "/api/sessions")
// - Value: returnURL (validated, e.g., "/api/sessions")
// - MaxAge: 3600 seconds (1 hour) - plenty of time for auth flow
// - Path: "/" - available to all endpoints
// - Domain: "" - current domain
Expand Down
10 changes: 10 additions & 0 deletions api/internal/events/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ type SessionCreateEvent struct {
PersistentHome bool `json:"persistent_home"`
IdleTimeout string `json:"idle_timeout"`
Metadata map[string]string `json:"metadata,omitempty"`
// Template configuration - used by controllers to create sessions
TemplateConfig *TemplateConfig `json:"template_config,omitempty"`
}

// TemplateConfig holds template configuration for session creation.
type TemplateConfig struct {
Image string `json:"image"`
VNCPort int `json:"vnc_port"`
DisplayName string `json:"display_name,omitempty"`
Env map[string]string `json:"env,omitempty"`
}

// SessionDeleteEvent is published when a session should be deleted.
Expand Down
29 changes: 26 additions & 3 deletions api/internal/handlers/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/streamspace/streamspace/api/internal/db"
"github.com/streamspace/streamspace/api/internal/events"
"github.com/streamspace/streamspace/api/internal/k8s"
"github.com/streamspace/streamspace/api/internal/models"
)

Expand All @@ -55,19 +56,24 @@ type ApplicationHandler struct {
db *db.Database
appDB *db.ApplicationDB
publisher *events.Publisher
k8sClient *k8s.Client
platform string
namespace string
}

// NewApplicationHandler creates a new application handler
func NewApplicationHandler(database *db.Database, publisher *events.Publisher, platform string) *ApplicationHandler {
func NewApplicationHandler(database *db.Database, publisher *events.Publisher, k8sClient *k8s.Client, platform string) *ApplicationHandler {
if platform == "" {
platform = events.PlatformKubernetes
}
namespace := "streamspace" // Default namespace
return &ApplicationHandler{
db: database,
appDB: db.NewApplicationDB(database.DB()),
publisher: publisher,
k8sClient: k8sClient,
platform: platform,
namespace: namespace,
}
}

Expand Down Expand Up @@ -279,8 +285,9 @@ func (h *ApplicationHandler) InstallApplication(c *gin.Context) {
// @Router /api/v1/applications/{id} [get]
func (h *ApplicationHandler) GetApplication(c *gin.Context) {
appID := c.Param("id")
ctx := c.Request.Context()

app, err := h.appDB.GetApplication(c.Request.Context(), appID)
app, err := h.appDB.GetApplication(ctx, appID)
if err != nil {
c.JSON(http.StatusNotFound, ErrorResponse{
Error: "Application not found",
Expand All @@ -289,8 +296,24 @@ func (h *ApplicationHandler) GetApplication(c *gin.Context) {
return
}

// Self-healing: Check if installation status needs to be updated
// If status is 'pending' or 'creating', check if the Template CRD exists
if app.InstallStatus == "pending" || app.InstallStatus == "creating" {
if h.k8sClient != nil && app.TemplateName != "" {
// Check if Template CRD exists in Kubernetes
_, err := h.k8sClient.GetTemplate(ctx, h.namespace, app.TemplateName)
if err == nil {
// Template exists! Update status to installed
h.updateInstallStatus(ctx, appID, "installed", "Template ready")
app.InstallStatus = "installed"
app.InstallMessage = "Template ready"
log.Printf("Updated installation status for %s to installed (template found)", appID)
}
}
}

// Get group access
groups, err := h.appDB.GetApplicationGroups(c.Request.Context(), appID)
groups, err := h.appDB.GetApplicationGroups(ctx, appID)
if err == nil {
app.Groups = groups
}
Expand Down
Loading
Loading