package platform import ( "context" "net/http" "time" ) func (s *service) canvasCapabilities(w http.ResponseWriter, r *http.Request, project, account string) error { if r.Method != http.MethodGet { reject(w, 405, "method_not_allowed") return nil } tx, err := s.db.BeginTx(r.Context(), nil) if err != nil { return err } defer tx.Rollback() kind, _, err := lockMaterial(r.Context(), tx, project, r.PathValue("workspace"), r.PathValue("material"), account) if err != nil { return err } if kind != "board" { reject(w, 400, "invalid_material") return nil } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() models, modelErr := s.canvasAI.Models(ctx) textAvailable, imageAvailable := false, false if modelErr == nil { for _, model := range models { if model.Available { textAvailable = textAvailable || canvasModelSupports(model, "text") imageAvailable = imageAvailable || canvasModelSupports(model, "image") } } } ai := response{"available": textAvailable || imageAvailable, "text": textAvailable, "image": imageAvailable} if modelErr != nil { ai["unavailable_reason"] = "codex_unavailable" } reply(w, 200, response{ "contract_version": "canvas-v2-2026-11-06", "supported_schema_versions": []int{1, 2}, "edge_features": response{"appearance": true, "markers": true, "bezier_chain": true, "animation_configuration": true, "execution_events": true}, "subgraphs": response{"create": true, "migrate": true, "undo": true, "members": true, "dissolve": true}, "ai": ai, "limits": response{"max_depth": 8, "max_nodes": 500, "max_edges": 2000, "max_frame_ports": 4000, "max_curve_knots": 64, "max_snapshot_bytes": canvasLimit, "max_active_tasks": 128, "max_run_steps": 64, "max_execution_depth": 32, "max_input_images": 4, "max_input_image_bytes": 20 << 20, "max_coordinate": 1000000}, "retention": response{"idempotency_min_seconds": 86400, "undo_receipt_seconds": 604800}, }) return nil }